Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into nsfw-api-mrf

This commit is contained in:
Lain Soykaf 2024-05-27 17:49:31 +04:00
commit 4325b1aec3
5904 changed files with 74362 additions and 23442 deletions

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.App do

View file

@ -1,113 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Benchmark do
import Mix.Pleroma
use Mix.Task
def run(["search"]) do
start_pleroma()
Benchee.run(%{
"search" => fn ->
Pleroma.Activity.search(nil, "cofe")
end
})
end
def run(["tag"]) do
start_pleroma()
Benchee.run(%{
"tag" => fn ->
%{"type" => "Create", "tag" => "cofe"}
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
})
end
def run(["render_timeline", nickname | _] = args) do
start_pleroma()
user = Pleroma.User.get_by_nickname(nickname)
activities =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("limit", 4096)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
|> Enum.reverse()
inputs = %{
"1 activity" => Enum.take_random(activities, 1),
"10 activities" => Enum.take_random(activities, 10),
"20 activities" => Enum.take_random(activities, 20),
"40 activities" => Enum.take_random(activities, 40),
"80 activities" => Enum.take_random(activities, 80)
}
inputs =
if Enum.at(args, 2) == "extended" do
Map.merge(inputs, %{
"200 activities" => Enum.take_random(activities, 200),
"500 activities" => Enum.take_random(activities, 500),
"2000 activities" => Enum.take_random(activities, 2000),
"4096 activities" => Enum.take_random(activities, 4096)
})
else
inputs
end
Benchee.run(
%{
"Standart rendering" => fn activities ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: activities,
for: user,
as: :activity
})
end
},
inputs: inputs
)
end
def run(["adapters"]) do
start_pleroma()
:ok =
Pleroma.Gun.Conn.open(
"https://httpbin.org/stream-bytes/1500",
:gun_connections
)
Process.sleep(1_500)
Benchee.run(
%{
"Without conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
pool: :no_pool,
receive_conn: false
)
end,
"Without conn and with pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], receive_conn: false)
end,
"With reused conn and without pool" => fn ->
{:ok, %Tesla.Env{}} =
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], pool: :no_pool)
end,
"With reused conn and with pool" => fn ->
{:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
end
},
parallel: 10
)
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Config do
@ -286,9 +286,7 @@ defmodule Mix.Tasks.Pleroma.Config do
file = File.open!(tmp_config_path)
shell_info(
"Saving database configuration settings to #{tmp_config_path}. Copy it to the #{
Path.dirname(config_path)
} manually."
"Saving database configuration settings to #{tmp_config_path}. Copy it to the #{Path.dirname(config_path)} manually."
)
write_config(file, tmp_config_path, opts)
@ -306,13 +304,8 @@ defmodule Mix.Tasks.Pleroma.Config do
System.cmd("mix", ["format", path])
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 config_header, do: "import Config\r\n\r\n"
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
defp write_and_delete(config, file, delete?) do
config

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.CountStatuses do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
@ -154,9 +154,8 @@ defmodule Mix.Tasks.Pleroma.Database do
|> join(:inner, [a], o in Object,
on:
fragment(
"(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
"(?->>'id') = associated_object_id((?))",
o.data,
a.data,
a.data
)
)
@ -194,7 +193,7 @@ defmodule Mix.Tasks.Pleroma.Database do
"ALTER DATABASE #{db} SET default_text_search_config = '#{tsconfig}';"
)
# non-exist config will not raise excpetion but only give >0 messages
# non-exist config will not raise exception but only give >0 messages
if length(msg) > 0 do
shell_info("Error: #{inspect(msg, pretty: true)}")
else
@ -209,7 +208,9 @@ defmodule Mix.Tasks.Pleroma.Database do
new.fts_content := to_tsvector(new.data->>'content');
RETURN new;
END
$$ LANGUAGE plpgsql"
$$ LANGUAGE plpgsql",
[],
timeout: :infinity
)
shell_info("Refresh RUM index")
@ -219,7 +220,9 @@ defmodule Mix.Tasks.Pleroma.Database do
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"CREATE INDEX objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); "
"CREATE INDEX CONCURRENTLY objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); ",
[],
timeout: :infinity
)
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Digest do
@ -30,7 +30,7 @@ defmodule Mix.Tasks.Pleroma.Digest do
shell_info("Digest email have been sent to #{nickname} (#{user.email})")
else
_ ->
shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
shell_info("Couldn't find any mentions for #{nickname} since #{last_digest_emailed_at}")
end
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Docs do

View file

@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Ecto do
@doc """

View file

@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
use Mix.Task

View file

@ -1,6 +1,6 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
use Mix.Task
@ -61,7 +61,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
Logger.configure(level: :info)
if opts[:env] == "test" do
Logger.info("Rollback succesfully")
Logger.info("Rollback successfully")
else
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts))

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Email do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
@ -111,7 +111,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{:ok, _} =
:zip.unzip(binary_archive,
cwd: pack_path,
cwd: String.to_charlist(pack_path),
file_list: files_to_unzip
)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Frontend do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Instance do
@ -34,7 +34,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
static_dir: :string,
listen_ip: :string,
listen_port: :string,
strip_uploads: :string,
strip_uploads_location: :string,
read_uploads_description: :string,
anonymize_uploads: :string,
dedupe_uploads: :string
],
@ -161,7 +162,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
|> Path.expand()
{strip_uploads_message, strip_uploads_default} =
{strip_uploads_location_message, strip_uploads_location_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
@ -170,12 +171,29 @@ defmodule Mix.Tasks.Pleroma.Instance do
"n"}
end
strip_uploads =
strip_uploads_location =
get_option(
options,
:strip_uploads,
strip_uploads_message,
strip_uploads_default
:strip_uploads_location,
strip_uploads_location_message,
strip_uploads_location_default
) === "y"
{read_uploads_description_message, read_uploads_description_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
read_uploads_description =
get_option(
options,
:read_uploads_description,
read_uploads_description_message,
read_uploads_description_default
) === "y"
anonymize_uploads =
@ -199,6 +217,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
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)
lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
{web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
@ -217,6 +236,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
secret: secret,
jwt_secret: jwt_secret,
signing_salt: signing_salt,
lv_signing_salt: lv_signing_salt,
web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
db_configurable?: db_configurable?,
@ -227,7 +247,8 @@ defmodule Mix.Tasks.Pleroma.Instance do
listen_port: listen_port,
upload_filters:
upload_filters(%{
strip: strip_uploads,
strip_location: strip_uploads_location,
read_description: read_uploads_description,
anonymize: anonymize_uploads,
dedupe: dedupe_uploads
})
@ -245,12 +266,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
config_dir = Path.dirname(config_path)
psql_dir = Path.dirname(psql_path)
# Note: Distros requiring group read (0o750) on those directories should
# pre-create the directories.
[config_dir, psql_dir, static_dir, uploads_dir]
|> Enum.reject(&File.exists?/1)
|> Enum.map(&File.mkdir_p!/1)
|> Enum.each(fn dir ->
File.mkdir_p!(dir)
File.chmod!(dir, 0o700)
end)
shell_info("Writing config to #{config_path}.")
# Sadly no fchmod(2) equivalent in Elixir…
File.touch!(config_path)
File.chmod!(config_path, 0o640)
File.write(config_path, result_config)
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
@ -263,14 +292,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
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."
" Please transfer your config to the database after running database migrations. Refer to \"Transferring 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" <>
(Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them."
Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them."
)
end
end
@ -295,12 +323,19 @@ defmodule Mix.Tasks.Pleroma.Instance do
defp upload_filters(filters) when is_map(filters) do
enabled_filters =
if filters.strip do
[Pleroma.Upload.Filter.Exiftool]
if filters.strip_location do
[Pleroma.Upload.Filter.Exiftool.StripLocation]
else
[]
end
enabled_filters =
if filters.read_description do
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
else
enabled_filters
end
enabled_filters =
if filters.anonymize do
enabled_filters ++ [Pleroma.Upload.Filter.AnonymizeFilename]
@ -317,6 +352,4 @@ defmodule Mix.Tasks.Pleroma.Instance do
enabled_filters
end
defp upload_filters(_), do: []
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.NotificationSettings do

View file

@ -1,8 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.OpenapiSpec do
def run([path]) do
# Load Pleroma application to get version info
Application.load(:pleroma)
spec = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!()
File.write(path, spec)
spec_json = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!()
# to get rid of the structs
spec_regened = spec_json |> Jason.decode!()
check_specs!(spec_regened)
File.write(path, spec_json)
end
defp check_specs!(spec) do
with :ok <- check_specs(spec) do
:ok
else
{_, errors} ->
IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"]))
Enum.map(errors, &IO.puts/1)
raise "Spec check failed"
end
end
def check_specs(spec) do
errors =
spec["paths"]
|> Enum.flat_map(fn {path, %{} = endpoints} ->
Enum.map(
endpoints,
fn {method, endpoint} ->
with :ok <- check_endpoint(spec, endpoint) do
:ok
else
error ->
"#{endpoint["operationId"]} (#{method} #{path}): #{error}"
end
end
)
|> Enum.reject(fn res -> res == :ok end)
end)
if errors == [] do
:ok
else
{:error, errors}
end
end
defp check_endpoint(spec, endpoint) do
valid_tags = available_tags(spec)
with {_, [_ | _] = tags} <- {:tags, endpoint["tags"]},
{_, []} <- {:unavailable, Enum.reject(tags, &(&1 in valid_tags))} do
:ok
else
{:tags, _} ->
"No tags specified"
{:unavailable, tags} ->
"Tags #{inspect(tags)} not available. Please add it in \"x-tagGroups\" in Pleroma.Web.ApiSpec"
end
end
defp available_tags(spec) do
spec["x-tagGroups"]
|> Enum.flat_map(fn %{"tags" => tags} -> tags end)
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RefreshCounterCache do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Relay do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxt do

View file

@ -0,0 +1,145 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
require Pleroma.Constants
import Mix.Pleroma
import Ecto.Query
import Pleroma.Search.Meilisearch,
only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete: 1]
def run(["index"]) do
start_pleroma()
Pleroma.HTML.compile_scrubbers()
meili_version =
(
{:ok, result} = meili_get("/version")
result["pkgVersion"]
)
# The ranking rule syntax was changed but nothing about that is mentioned in the changelog
if not Version.match?(meili_version, ">= 0.25.0") do
raise "Meilisearch <0.24.0 not supported"
end
{:ok, _} =
meili_post(
"/indexes/objects/settings/ranking-rules",
[
"published:desc",
"words",
"exactness",
"proximity",
"typo",
"attribute",
"sort"
]
)
{:ok, _} =
meili_post(
"/indexes/objects/settings/searchable-attributes",
[
"content"
]
)
IO.puts("Created indices. Starting to insert posts.")
chunk_size = Pleroma.Config.get([Pleroma.Search.Meilisearch, :initial_indexing_chunk_size])
Pleroma.Repo.transaction(
fn ->
query =
from(Pleroma.Object,
# Only index public and unlisted posts which are notes and have some text
where:
fragment("data->>'type' = 'Note'") and
(fragment("data->'to' \\? ?", ^Pleroma.Constants.as_public()) or
fragment("data->'cc' \\? ?", ^Pleroma.Constants.as_public())),
order_by: [desc: fragment("data->'published'")]
)
count = query |> Pleroma.Repo.aggregate(:count, :data)
IO.puts("Entries to index: #{count}")
Pleroma.Repo.stream(
query,
timeout: :infinity
)
|> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
|> Stream.filter(fn o -> not is_nil(o) end)
|> Stream.chunk_every(chunk_size)
|> Stream.transform(0, fn objects, acc ->
new_acc = acc + Enum.count(objects)
# Reset to the beginning of the line and rewrite it
IO.write("\r")
IO.write("Indexed #{new_acc} entries")
{[objects], new_acc}
end)
|> Stream.each(fn objects ->
result =
meili_put(
"/indexes/objects/documents",
objects
)
with {:ok, res} <- result do
if not Map.has_key?(res, "uid") do
IO.puts("\nFailed to index: #{inspect(result)}")
end
else
e -> IO.puts("\nFailed to index due to network error: #{inspect(e)}")
end
end)
|> Stream.run()
end,
timeout: :infinity
)
IO.write("\n")
end
def run(["clear"]) do
start_pleroma()
meili_delete("/indexes/objects/documents")
end
def run(["show-keys", master_key]) do
start_pleroma()
endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
{:ok, result} =
Pleroma.HTTP.get(
Path.join(endpoint, "/keys"),
[{"Authorization", "Bearer #{master_key}"}]
)
decoded = Jason.decode!(result.body)
if decoded["results"] do
Enum.each(decoded["results"], fn %{"description" => desc, "key" => key} ->
IO.puts("#{desc}: #{key}")
end)
else
IO.puts("Error fetching the keys, check the master key is correct: #{inspect(decoded)}")
end
end
def run(["stats"]) do
start_pleroma()
{:ok, result} = meili_get("/indexes/objects/stats")
IO.puts("Number of entries: #{result["numberOfDocuments"]}")
IO.puts("Indexing? #{result["isIndexing"]}")
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Uploads do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
@ -51,9 +51,7 @@ defmodule Mix.Tasks.Pleroma.User do
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
- password: #{
if(generated_password?, do: "[generated; a reset link will be created]", else: password)
}
- password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)}
- name: #{name}
- bio: #{bio}
- moderator: #{if(moderator?, do: "true", else: "false")}
@ -114,15 +112,10 @@ defmodule Mix.Tasks.Pleroma.User do
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
shell_info("Generated password reset token for #{user.nickname}")
IO.puts(
"URL: #{
Pleroma.Web.Router.Helpers.reset_password_url(
Pleroma.Web.Endpoint,
:reset,
token.token
)
}"
)
url =
Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, :reset, token.token)
IO.puts("URL: #{url}")
else
_ ->
shell_error("No local user #{nickname}")
@ -321,9 +314,7 @@ defmodule Mix.Tasks.Pleroma.User do
end
shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}"
)
end)
end
@ -424,15 +415,45 @@ defmodule Mix.Tasks.Pleroma.User do
users
|> Enum.each(fn user ->
shell_info(
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{
user.is_locked
}, is_active: #{user.is_active}"
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}"
)
end)
end)
|> Stream.run()
end
def run(["fix_follow_state", local_user, remote_user]) do
start_pleroma()
with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
{:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
{:follow_data, %{data: %{"state" => request_state}}} <-
{:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
calculated_state = User.following?(local, remote)
shell_info(
"Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
)
if calculated_state == false && request_state == "accept" do
shell_info("Discrepancy found, fixing")
Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
shell_info("Relationship fixed")
else
shell_info("No discrepancy found")
end
else
{:local, _} ->
shell_error("No local user #{local_user}")
{:remote, _} ->
shell_error("No remote user #{remote_user}")
{:follow_data, _} ->
shell_error("No follow data for #{local_user} and #{remote_user}")
end
end
defp set_moderator(user, value) do
{:ok, user} =
user

View file

@ -1,94 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Phoenix.Transports.WebSocket.Raw do
import Plug.Conn,
only: [
fetch_query_params: 1,
send_resp: 3
]
alias Phoenix.Socket.Transport
def default_config do
[
timeout: 60_000,
transport_log: false,
cowboy: Phoenix.Endpoint.CowboyWebSocket
]
end
def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport)
conn =
conn
|> fetch_query_params
|> Transport.transport_log(opts[:transport_log])
|> Transport.force_ssl(handler, endpoint, opts)
|> Transport.check_origin(handler, endpoint, opts)
case conn do
%{halted: false} = conn ->
case handler.connect(%{
endpoint: endpoint,
transport: transport,
options: [serializer: nil],
params: conn.params
}) do
{:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}}
:error ->
send_resp(conn, :forbidden, "")
{:error, conn}
end
_ ->
{:error, conn}
end
end
def init(conn, _) do
send_resp(conn, :bad_request, "")
{:error, conn}
end
def ws_init({socket, config}) do
Process.flag(:trap_exit, true)
{:ok, %{socket: socket}, config[:timeout]}
end
def ws_handle(op, data, state) do
state.socket.handler
|> apply(:handle, [op, data, state])
|> case do
{op, data} ->
{:reply, {op, data}, state}
{op, data, state} ->
{:reply, {op, data}, state}
%{} = state ->
{:ok, state}
_ ->
{:ok, state}
end
end
def ws_info({_, _} = tuple, state) do
{:reply, tuple, state}
end
def ws_info(_tuple, state), do: {:ok, state}
def ws_close(state) do
ws_handle(:closed, :normal, state)
end
def ws_terminate(reason, state) do
ws_handle(:closed, reason, state)
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity do
@ -53,7 +53,7 @@ defmodule Pleroma.Activity do
#
# ```
# |> join(:inner, [activity], o in Object,
# on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')",
# on: fragment("(?->>'id') = associated_object_id((?))",
# o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object])
# ```
@ -69,9 +69,8 @@ defmodule Pleroma.Activity do
join(query, join_type, [activity], o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
"(?->>'id') = associated_object_id(?)",
o.data,
activity.data,
activity.data
),
as: :object
@ -292,7 +291,8 @@ defmodule Pleroma.Activity do
get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
def normalize(%Activity{data: %{"id" => ap_id}}), do: get_by_ap_id_with_object(ap_id)
def normalize(%{"id" => ap_id}), do: get_by_ap_id_with_object(ap_id)
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
def normalize(_), do: nil
@ -301,7 +301,7 @@ defmodule Pleroma.Activity do
|> Queries.by_object_id()
|> Queries.exclude_type("Delete")
|> select([u], u)
|> Repo.delete_all()
|> Repo.delete_all(timeout: :infinity)
|> elem(1)
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id
@ -313,13 +313,15 @@ defmodule Pleroma.Activity do
def delete_all_by_object_ap_id(_), do: nil
defp purge_web_resp_cache(%Activity{} = activity) do
%{path: path} = URI.parse(activity.data["id"])
@cachex.del(:web_resp_cache, path)
defp purge_web_resp_cache(%Activity{data: %{"id" => id}} = activity) when is_binary(id) do
with %{path: path} <- URI.parse(id) do
@cachex.del(:web_resp_cache, path)
end
activity
end
defp purge_web_resp_cache(nil), do: nil
defp purge_web_resp_cache(activity), do: activity
def follow_accepted?(
%Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
@ -359,14 +361,14 @@ defmodule Pleroma.Activity do
end
def restrict_deactivated_users(query) do
deactivated_users =
from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
|> Repo.all()
Activity.Queries.exclude_authors(query, deactivated_users)
query
|> join(:inner, [activity], user in User,
as: :user,
on: activity.actor == user.ap_id and user.is_active == true
)
end
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
defdelegate search(user, query, options \\ []), to: Pleroma.Search.DatabaseSearch
def direct_conversation_id(activity, for_user) do
alias Pleroma.Conversation.Participation

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.HTML do
@ -8,6 +8,40 @@ defmodule Pleroma.Activity.HTML do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
# We store a list of cache keys related to an activity in a
# separate cache, scrubber_management_cache. It has the same
# size as scrubber_cache (see application.ex). Every time we add
# a cache to scrubber_cache, we update scrubber_management_cache.
#
# The most recent write of a certain key in the management cache
# is the same as the most recent write of any record related to that
# key in the main cache.
# Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ),
# this means when the management cache is evicted by cachex, all
# related records in the main cache will also have been evicted.
defp get_cache_keys_for(activity_id) do
with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do
list
else
_ -> []
end
end
def add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id)
unless additional_key in current do
@cachex.put(:scrubber_management_cache, activity_id, [additional_key | current])
end
end
def invalidate_cache_for(activity_id) do
keys = get_cache_keys_for(activity_id)
Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
@cachex.del(:scrubber_management_cache, activity_id)
end
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
@ -19,6 +53,8 @@ defmodule Pleroma.Activity.HTML do
@cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Object.normalize(activity, fetch: false)
add_cache_key_for(activity.id, key)
HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Ir.Topics do
@ -13,6 +13,14 @@ defmodule Pleroma.Activity.Ir.Topics do
|> List.flatten()
end
defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Delete"}}) do
["user", "user:pleroma_chat"]
end
defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Create"}}) do
[]
end
defp generate_topics(%{data: %{"type" => "Answer"}}, _) do
[]
end
@ -21,7 +29,7 @@ defmodule Pleroma.Activity.Ir.Topics do
["user", "list"] ++ visibility_tags(object, activity)
end
defp visibility_tags(object, activity) do
defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do
case Visibility.get_visibility(activity) do
"public" ->
if activity.local do
@ -31,6 +39,10 @@ defmodule Pleroma.Activity.Ir.Topics do
end
|> item_creation_tags(object, activity)
"local" ->
["public:local"]
|> item_creation_tags(object, activity)
"direct" ->
["direct"]
@ -39,6 +51,10 @@ defmodule Pleroma.Activity.Ir.Topics do
end
end
defp visibility_tags(_object, _activity) do
[]
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
@ -63,7 +79,18 @@ defmodule Pleroma.Activity.Ir.Topics do
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"]
defp attachment_topics(_object, %{local: true} = activity) do
case Visibility.get_visibility(activity) do
"public" ->
["public:media", "public:local:media"]
"local" ->
["public:local:media"]
_ ->
[]
end
end
defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Queries do
@ -9,7 +9,7 @@ defmodule Pleroma.Activity.Queries do
import Ecto.Query, only: [from: 2, where: 3]
@type query :: Ecto.Queryable.t() | Activity.t()
@type query :: Ecto.Queryable.t() | Pleroma.Activity.t()
alias Pleroma.Activity
alias Pleroma.User
@ -52,8 +52,7 @@ defmodule Pleroma.Activity.Queries do
activity in query,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
activity.data,
"associated_object_id((?)) = ANY(?)",
activity.data,
^object_ids
)
@ -64,8 +63,7 @@ defmodule Pleroma.Activity.Queries do
from(activity in query,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
"associated_object_id((?)) = ?",
activity.data,
^object_id
)

162
lib/pleroma/announcement.ex Normal file
View file

@ -0,0 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Announcement do
use Ecto.Schema
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
import Ecto.Query
alias Pleroma.AnnouncementReadRelationship
alias Pleroma.Repo
@type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "announcements" do
field(:data, :map)
field(:starts_at, :utc_datetime)
field(:ends_at, :utc_datetime)
field(:rendered, :map)
timestamps(type: :utc_datetime)
end
@doc "Generates changeset for %Pleroma.Announcement{}"
@spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
def changeset(announcement \\ %__MODULE__{}, params \\ %{data: %{}}) do
announcement
|> cast(validate_params(announcement, params), [:data, :starts_at, :ends_at, :rendered])
|> validate_required([:data])
end
defp validate_params(announcement, params) do
base_data =
%{
"content" => "",
"all_day" => false
}
|> Map.merge((announcement && announcement.data) || %{})
merged_data =
Map.merge(base_data, params.data)
|> Map.take(["content", "all_day"])
params
|> Map.merge(%{data: merged_data})
|> add_rendered_properties()
end
def add_rendered_properties(params) do
{content_html, _, _} =
Pleroma.Web.CommonAPI.Utils.format_input(params.data["content"], "text/plain",
mentions_format: :full
)
rendered = %{
"content" => content_html
}
params
|> Map.put(:rendered, rendered)
end
def add(params) do
changeset = changeset(%__MODULE__{}, params)
Repo.insert(changeset)
end
def update(announcement, params) do
changeset = changeset(announcement, params)
Repo.update(changeset)
end
def list_all do
__MODULE__
|> Repo.all()
end
def list_paginated(%{limit: limited_number, offset: offset_number}) do
__MODULE__
|> limit(^limited_number)
|> offset(^offset_number)
|> Repo.all()
end
def get_by_id(id) do
Repo.get_by(__MODULE__, id: id)
end
def delete_by_id(id) do
with announcement when not is_nil(announcement) <- get_by_id(id),
{:ok, _} <- Repo.delete(announcement) do
:ok
else
_ ->
:error
end
end
def read_by?(announcement, user) do
AnnouncementReadRelationship.exists?(user, announcement)
end
def mark_read_by(announcement, user) do
AnnouncementReadRelationship.mark_read(user, announcement)
end
def render_json(announcement, opts \\ []) do
extra_params =
case Keyword.fetch(opts, :for) do
{:ok, user} when not is_nil(user) ->
%{read: read_by?(announcement, user)}
_ ->
%{}
end
admin_extra_params =
case Keyword.fetch(opts, :admin) do
{:ok, true} ->
%{pleroma: %{raw_content: announcement.data["content"]}}
_ ->
%{}
end
base = %{
id: announcement.id,
content: announcement.rendered["content"],
starts_at: announcement.starts_at,
ends_at: announcement.ends_at,
all_day: announcement.data["all_day"],
published_at: announcement.inserted_at,
updated_at: announcement.updated_at,
mentions: [],
statuses: [],
tags: [],
emojis: [],
reactions: []
}
base
|> Map.merge(extra_params)
|> Map.merge(admin_extra_params)
end
# "visible" means:
# starts_at < time < ends_at
def list_all_visible_when(time) do
__MODULE__
|> where([a], is_nil(a.starts_at) or a.starts_at < ^time)
|> where([a], is_nil(a.ends_at) or a.ends_at > ^time)
|> Repo.all()
end
def list_all_visible do
list_all_visible_when(DateTime.now("Etc/UTC") |> elem(1))
end
end

View file

@ -0,0 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.AnnouncementReadRelationship do
use Ecto.Schema
import Ecto.Changeset
alias FlakeId.Ecto.CompatType
alias Pleroma.Announcement
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
schema "announcement_read_relationships" do
belongs_to(:user, User, type: CompatType)
belongs_to(:announcement, Announcement, type: CompatType)
timestamps(updated_at: false)
end
def mark_read(user, announcement) do
%__MODULE__{}
|> cast(%{user_id: user.id, announcement_id: announcement.id}, [:user_id, :announcement_id])
|> validate_required([:user_id, :announcement_id])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:announcement_id)
|> unique_constraint([:user_id, :announcement_id])
|> Repo.insert()
end
def mark_unread(user, announcement) do
with relationship <- get(user, announcement),
{:exists, true} <- {:exists, not is_nil(relationship)},
{:ok, _} <- Repo.delete(relationship) do
:ok
else
{:exists, false} ->
:ok
_ ->
:error
end
end
def get(user, announcement) do
Repo.get_by(__MODULE__, user_id: user.id, announcement_id: announcement.id)
end
def exists?(user, announcement) do
not is_nil(get(user, announcement))
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do
@ -14,7 +14,6 @@ defmodule Pleroma.Application do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
@mix_env Mix.env()
def name, do: @name
def version, do: @version
@ -54,13 +53,17 @@ defmodule Pleroma.Application do
Config.DeprecationWarnings.warn()
Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.ApplicationRequirements.verify!()
setup_instrumenters()
load_custom_modules()
Pleroma.Docs.JSON.compile()
limiters_setup()
adapter = Application.get_env(:tesla, :adapter)
if match?({Tesla.Adapter.Finch, _}, adapter) do
Logger.info("Starting Finch")
Finch.start_link(name: MyFinch)
end
if adapter == Tesla.Adapter.Gun do
if version = Pleroma.OTPVersion.version() do
[major, minor] =
@ -86,13 +89,15 @@ defmodule Pleroma.Application do
# Define workers and child supervisors to be supervised
children =
[
Pleroma.PromEx,
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
Pleroma.Web.Plugs.RateLimiter.Supervisor
Pleroma.Web.Plugs.RateLimiter.Supervisor,
{Task.Supervisor, name: Pleroma.TaskSupervisor}
] ++
cachex_children() ++
http_children(adapter, @mix_env) ++
http_children(adapter) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
@ -100,36 +105,22 @@ defmodule Pleroma.Application do
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
task_children(@mix_env) ++
dont_run_in_test(@mix_env) ++
task_children() ++
streamer_registry() ++
background_migrators() ++
shout_child(shout_enabled?()) ++
[Pleroma.Gopher.Server]
[Pleroma.Gopher.Server] ++
[Pleroma.Search.Healthcheck]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
result = Supervisor.start_link(children, opts)
# If we have a lot of caches, default max_restarts can cause test
# resets to fail.
# Go for the default 3 unless we're in test
max_restarts = Application.get_env(:pleroma, __MODULE__)[:max_restarts]
set_postgres_server_version()
result
end
defp set_postgres_server_version do
version =
with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
{num, _} <- Float.parse(version) do
num
else
e ->
Logger.warn(
"Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
)
9.6
end
:persistent_term.put({Pleroma.Repo, :postgres_version}, version)
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
Supervisor.start_link(children, opts)
end
def load_custom_modules do
@ -143,7 +134,7 @@ defmodule Pleroma.Application do
raise "Invalid custom modules"
{:ok, modules, _warnings} ->
if @mix_env != :test do
if Application.get_env(:pleroma, __MODULE__)[:load_custom_modules] do
Enum.each(modules, fn mod ->
Logger.info("Custom module loaded: #{inspect(mod)}")
end)
@ -154,29 +145,6 @@ defmodule Pleroma.Application do
end
end
defp setup_instrumenters do
require Prometheus.Registry
if Application.get_env(:prometheus, Pleroma.Repo.Instrumenter) do
:ok =
:telemetry.attach(
"prometheus-ecto",
[:pleroma, :repo, :query],
&Pleroma.Repo.Instrumenter.handle_event/4,
%{}
)
Pleroma.Repo.Instrumenter.setup()
end
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
# Note: disabled until prometheus-phx is integrated into prometheus-phoenix:
# Pleroma.Web.Endpoint.Instrumenter.setup()
PrometheusPhx.setup()
end
defp cachex_children do
[
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
@ -184,15 +152,19 @@ defmodule Pleroma.Application do
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("scrubber_management", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
build_cachex("failed_media_helper_url", default_ttl: :timer.minutes(15), limit: 2_500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("chat_message_id_idempotency_key",
expiration: chat_message_id_idempotency_key_expiration(),
limit: 500_000
)
),
build_cachex("rel_me", limit: 2500),
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000)
]
end
@ -218,23 +190,30 @@ defmodule Pleroma.Application do
defp shout_enabled?, do: Config.get([:shout, :enabled])
defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
defp dont_run_in_test(_) do
[
{Registry,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
]}
] ++ background_migrators()
defp streamer_registry do
if Application.get_env(:pleroma, __MODULE__)[:streamer_registry] do
[
{Registry,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
]}
]
else
[]
end
end
defp background_migrators do
[
Pleroma.Migrators.HashtagsTableMigrator
]
if Application.get_env(:pleroma, __MODULE__)[:background_migrators] do
[
Pleroma.Migrators.HashtagsTableMigrator,
Pleroma.Migrators.ContextObjectsDeletionMigrator
]
else
[]
end
end
defp shout_child(true) do
@ -246,37 +225,43 @@ defmodule Pleroma.Application do
defp shout_child(_), do: []
defp task_children(:test) do
[
defp task_children do
children = [
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
}
]
end
defp task_children(_) do
[
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :internal_fetch_init,
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
restart: :temporary
}
]
if Application.get_env(:pleroma, __MODULE__)[:internal_fetch] do
children ++
[
%{
id: :internal_fetch_init,
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
restart: :temporary
}
]
else
children
end
end
# start hackney and gun pools in tests
defp http_children(_, :test) do
http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
defp http_children(adapter) do
if Application.get_env(:pleroma, __MODULE__)[:test_http_pools] do
http_children_hackney() ++ http_children_gun()
else
cond do
match?(Tesla.Adapter.Hackney, adapter) -> http_children_hackney()
match?(Tesla.Adapter.Gun, adapter) -> http_children_gun()
true -> []
end
end
end
defp http_children(Tesla.Adapter.Hackney, _) do
defp http_children_hackney do
pools = [:federation, :media]
pools =
@ -292,18 +277,20 @@ defmodule Pleroma.Application do
end
end
defp http_children(Tesla.Adapter.Gun, _) do
defp http_children_gun do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
defp http_children(_, _), do: []
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
[Pleroma.Web.RichMedia.Helpers, Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
[
Pleroma.Web.RichMedia.Helpers,
Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
Pleroma.Search
]
|> Enum.each(fn module ->
mod_config = Keyword.get(config, module, [])

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ApplicationRequirements do
@ -7,7 +7,10 @@ defmodule Pleroma.ApplicationRequirements do
The module represents the collection of validations to runs before start server.
"""
defmodule VerifyError, do: defexception([:message])
defmodule VerifyError do
defexception([:message])
@type t :: %__MODULE__{}
end
alias Pleroma.Config
alias Pleroma.Helpers.MediaHelper
@ -25,6 +28,7 @@ defmodule Pleroma.ApplicationRequirements do
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
|> check_mrfs()
|> handle_result()
end
@ -34,7 +38,7 @@ defmodule Pleroma.ApplicationRequirements do
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do
Logger.warn("""
Logger.warning("""
To send welcome emails, you need to enable the mailer.
Welcome emails will NOT be sent with the current config.
@ -53,7 +57,7 @@ defmodule Pleroma.ApplicationRequirements do
def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Emails.Mailer.enabled?() do
Logger.warn("""
Logger.warning("""
Account activation is required, but the mailer is disabled.
Users will NOT be able to confirm their accounts with this config.
Either disable account activation or enable the mailer.
@ -164,11 +168,10 @@ defmodule Pleroma.ApplicationRequirements do
defp check_system_commands!(:ok) do
filter_commands_statuses = [
check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
check_filter(Pleroma.Upload.Filter.Exiftool.StripLocation, "exiftool"),
check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "convert"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "ffprobe")
]
@ -194,8 +197,6 @@ defmodule Pleroma.ApplicationRequirements do
end
end
defp check_system_commands!(result), do: result
defp check_repo_pool_size!(:ok) do
if Pleroma.Config.get([Pleroma.Repo, :pool_size], 10) != 10 and
not Pleroma.Config.get([:dangerzone, :override_repo_pool_size], false) do
@ -234,4 +235,25 @@ defmodule Pleroma.ApplicationRequirements do
true
end
end
defp check_mrfs(:ok) do
mrfs = Config.get!([:mrf, :policies])
missing_mrfs =
Enum.reduce(mrfs, [], fn x, acc ->
if Code.ensure_compiled(x) do
acc
else
acc ++ [x]
end
end)
if Enum.empty?(missing_mrfs) do
:ok
else
{:error, "The following MRF modules are configured but missing: #{inspect(missing_mrfs)}"}
end
end
defp check_mrfs(result), do: result
end

View file

@ -1,20 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
alias Pleroma.User
alias Pleroma.Web.Plugs.AuthenticationPlug
def authenticate(username, password) do
username = to_string(username)
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
AuthenticationPlug.checkpw(password, user.password_hash)
else
_e -> false
end
end
end

View file

@ -1,151 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BBS.Handler do
use Sshd.ShellHandler
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
def on_shell(username, _pubkey, _ip, _port) do
:ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!")
user = Pleroma.User.get_cached_by_nickname(to_string(username))
Logger.debug("#{inspect(user)}")
loop(run_state(user: user))
end
def on_connect(username, ip, port, method) do
Logger.debug(fn ->
"""
Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{
inspect(port)
} using #{inspect(method)}
"""
end)
end
def on_disconnect(username, ip, port) do
Logger.debug(fn ->
"Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}"
end)
end
defp loop(state) do
self_pid = self()
counter = state.counter
prefix = state.prefix
user = state.user
input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end)
wait_input(state, input)
end
def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HTML.strip_tags(status.content))
IO.puts("")
end
def handle_command(state, "help") do
IO.puts("Available commands:")
IO.puts("help - This help")
IO.puts("home - Show the home timeline")
IO.puts("p <text> - Post the given text")
IO.puts("r <id> <text> - Reply to the post with the given id")
IO.puts("quit - Quit")
state
end
def handle_command(%{user: user} = state, "r " <> text) do
text = String.trim(text)
[activity_id, rest] = String.split(text, " ", parts: 2)
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
end
state
end
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
end
state
end
def handle_command(state, "home") do
user = state.user
params =
%{}
|> Map.put(:type, ["Create"])
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:user, user)
activities =
[user.ap_id | Pleroma.User.following(user)]
|> ActivityPub.fetch_activities(params)
Enum.each(activities, fn activity ->
puts_activity(activity)
end)
state
end
def handle_command(state, command) do
IO.puts("Unknown command '#{command}'")
state
end
defp wait_input(state, input) do
receive do
{:input, ^input, "quit\n"} ->
IO.puts("Exiting...")
{:input, ^input, code} when is_binary(code) ->
code = String.trim(code)
state = handle_command(state, code)
loop(%{state | counter: state.counter + 1})
{:error, :interrupted} ->
IO.puts("Caught Ctrl+C...")
loop(%{state | counter: state.counter + 1})
{:input, ^input, msg} ->
:ok = Logger.warn("received unknown message: #{inspect(msg)}")
loop(%{state | counter: state.counter + 1})
end
end
defp run_state(opts) do
%{prefix: "pleroma", counter: 1, user: opts[:user]}
end
defp io_get(pid, prefix, counter, username) do
prompt = prompt(prefix, counter, username)
send(pid, {:input, self(), IO.gets(:stdio, prompt)})
end
defp prompt(prefix, counter, username) do
prompt = "#{username}@#{prefix}:#{counter}>"
prompt <> " "
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Bookmark do
@ -10,6 +10,7 @@ defmodule Pleroma.Bookmark do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
alias Pleroma.Repo
alias Pleroma.User
@ -18,33 +19,46 @@ defmodule Pleroma.Bookmark do
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
timestamps()
end
@spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do
@spec create(Ecto.UUID.t(), Ecto.UUID.t()) ::
{:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def create(user_id, activity_id, folder_id \\ nil) do
attrs = %{
user_id: user_id,
activity_id: activity_id
activity_id: activity_id,
folder_id: folder_id
}
%Bookmark{}
|> cast(attrs, [:user_id, :activity_id])
|> cast(attrs, [:user_id, :activity_id, :folder_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert()
|> Repo.insert(
on_conflict: [set: [folder_id: folder_id]],
conflict_target: [:user_id, :activity_id]
)
end
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
@spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
def for_user_query(user_id, folder_id \\ nil) do
Bookmark
|> where(user_id: ^user_id)
|> maybe_filter_by_folder(folder_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
defp maybe_filter_by_folder(query, nil), do: query
defp maybe_filter_by_folder(query, folder_id) do
query
|> where(folder_id: ^folder_id)
end
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
@ -52,8 +66,8 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
end
@spec destroy(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
@spec destroy(Ecto.UUID.t(), Ecto.UUID.t()) ::
{:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
@ -62,4 +76,11 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
|> Repo.delete()
end
def set_folder(bookmark, folder_id) do
bookmark
|> cast(%{folder_id: folder_id}, [:folder_id])
|> validate_required([:folder_id])
|> Repo.update()
end
end

View file

@ -0,0 +1,115 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BookmarkFolder do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.BookmarkFolder
alias Pleroma.Emoji
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "bookmark_folders" do
field(:name, :string)
field(:emoji, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id)
def create(user_id, name, emoji \\ nil) do
%BookmarkFolder{}
|> cast(
%{
user_id: user_id,
name: name,
emoji: emoji
},
[:user_id, :name, :emoji]
)
|> validate_required([:user_id, :name])
|> fix_emoji()
|> validate_emoji()
|> unique_constraint([:user_id, :name])
|> Repo.insert()
end
def update(folder_id, name, emoji \\ nil) do
get_by_id(folder_id)
|> cast(
%{
name: name,
emoji: emoji
},
[:name, :emoji]
)
|> fix_emoji()
|> validate_emoji()
|> unique_constraint([:user_id, :name])
|> Repo.update()
end
defp fix_emoji(changeset) do
with {:emoji_field, emoji} when is_binary(emoji) <-
{:emoji_field, get_field(changeset, :emoji)},
{:fixed_emoji, emoji} <-
{:fixed_emoji,
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()} do
put_change(changeset, :emoji, emoji)
else
{:emoji_field, _} -> changeset
end
end
defp validate_emoji(changeset) do
validate_change(changeset, :emoji, fn
:emoji, nil ->
[]
:emoji, emoji ->
if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do
[]
else
[emoji: "Invalid emoji"]
end
end)
end
defp valid_local_custom_emoji?(emoji) do
with %{file: _path} <- Emoji.get(emoji) do
true
else
_ -> false
end
end
def delete(folder_id) do
BookmarkFolder
|> Repo.get_by(id: folder_id)
|> Repo.delete()
end
def for_user(user_id) do
BookmarkFolder
|> where(user_id: ^user_id)
|> Repo.all()
end
def belongs_to_user?(folder_id, user_id) do
BookmarkFolder
|> where(id: ^folder_id, user_id: ^user_id)
|> Repo.exists?()
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Caching do
@ -8,10 +8,13 @@ defmodule Pleroma.Caching do
@callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()}
@callback fetch!(Cachex.cache(), any(), function() | nil) :: any()
@callback fetch(Cachex.cache(), any(), function() | nil) ::
{atom(), any()} | {atom(), any(), any()}
# @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback stream!(Cachex.cache(), any()) :: Enumerable.t()
@callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback expire(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback execute!(Cachex.cache(), function()) :: any()
@callback get_and_update(Cachex.cache(), any(), function()) ::

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Kocaptcha do
@ -29,7 +29,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
@impl Service
def validate(_token, captcha, answer_data) do
# Here the token is unsed, because the unencrypted captcha answer is just passed to method
# Here the token is unused, because the unencrypted captcha answer is just passed to method
if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok,

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Service do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat do
@ -42,7 +42,7 @@ defmodule Pleroma.Chat do
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
end
@spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) ::
@spec get_by_user_and_id(User.t(), Ecto.UUID.t()) ::
{:ok, t()} | {:error, :not_found}
def get_by_user_and_id(%User{id: user_id}, id) do
from(c in __MODULE__,
@ -52,17 +52,17 @@ defmodule Pleroma.Chat do
|> Repo.find_resource()
end
@spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
@spec get_by_id(Ecto.UUID.t()) :: t() | nil
def get_by_id(id) do
Repo.get(__MODULE__, id)
end
@spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
@spec get(Ecto.UUID.t(), String.t()) :: t() | nil
def get(user_id, recipient) do
Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
end
@spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
@spec get_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_create(user_id, recipient) do
%__MODULE__{}
@ -75,7 +75,7 @@ defmodule Pleroma.Chat do
)
end
@spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
@spec bump_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def bump_or_create(user_id, recipient) do
%__MODULE__{}
@ -87,7 +87,7 @@ defmodule Pleroma.Chat do
)
end
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
@spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
from(c in Chat,
where: c.user_id == ^user_id,

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat.MessageReference do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Clippy do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do
@ -20,9 +20,180 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
]
def check_exiftool_filter do
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
if Pleroma.Upload.Filter.Exiftool in filters do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool]
```
Is now
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
```
""")
new_config =
filters
|> Enum.map(fn
Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripLocation
filter -> filter
end)
Config.put([Pleroma.Upload, :filters], new_config)
:error
else
:ok
end
end
def check_simple_policy_tuples do
has_strings =
Config.get([:mrf_simple])
|> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end)
if has_strings do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
```
config :pleroma, :mrf_simple,
media_removal: ["instance.tld"],
media_nsfw: ["instance.tld"],
federated_timeline_removal: ["instance.tld"],
report_removal: ["instance.tld"],
reject: ["instance.tld"],
followers_only: ["instance.tld"],
accept: ["instance.tld"],
avatar_removal: ["instance.tld"],
banner_removal: ["instance.tld"],
reject_deletes: ["instance.tld"]
```
Is now
```
config :pleroma, :mrf_simple,
media_removal: [{"instance.tld", "Reason for media removal"}],
media_nsfw: [{"instance.tld", "Reason for media nsfw"}],
federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}],
report_removal: [{"instance.tld", "Reason for report removal"}],
reject: [{"instance.tld", "Reason for reject"}],
followers_only: [{"instance.tld", "Reason for followers only"}],
accept: [{"instance.tld", "Reason for accept"}],
avatar_removal: [{"instance.tld", "Reason for avatar removal"}],
banner_removal: [{"instance.tld", "Reason for banner removal"}],
reject_deletes: [{"instance.tld", "Reason for reject deletes"}]
```
""")
new_config =
Config.get([:mrf_simple])
|> Enum.map(fn {k, v} ->
{k,
Enum.map(v, fn
{instance, reason} -> {instance, reason}
instance -> {instance, ""}
end)}
end)
Config.put([:mrf_simple], new_config)
:error
else
:ok
end
end
def check_quarantined_instances_tuples do
has_strings = Config.get([:instance, :quarantined_instances]) |> Enum.any?(&is_binary/1)
if has_strings do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
```
config :pleroma, :instance,
quarantined_instances: ["instance.tld"]
```
Is now
```
config :pleroma, :instance,
quarantined_instances: [{"instance.tld", "Reason for quarantine"}]
```
""")
new_config =
Config.get([:instance, :quarantined_instances])
|> Enum.map(fn
{instance, reason} -> {instance, reason}
instance -> {instance, ""}
end)
Config.put([:instance, :quarantined_instances], new_config)
:error
else
:ok
end
end
def check_transparency_exclusions_tuples do
has_strings = Config.get([:mrf, :transparency_exclusions]) |> Enum.any?(&is_binary/1)
if has_strings do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later:
```
config :pleroma, :mrf,
transparency_exclusions: ["instance.tld"]
```
Is now
```
config :pleroma, :mrf,
transparency_exclusions: [{"instance.tld", "Reason to exclude transparency"}]
```
""")
new_config =
Config.get([:mrf, :transparency_exclusions])
|> Enum.map(fn
{instance, reason} -> {instance, reason}
instance -> {instance, ""}
end)
Config.put([:mrf, :transparency_exclusions], new_config)
:error
else
:ok
end
end
def check_hellthread_threshold do
if Config.get([:mrf_hellthread, :threshold]) do
Logger.warn("""
Logger.warning("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
""")
@ -34,20 +205,25 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
def warn do
with :ok <- check_hellthread_threshold(),
:ok <- check_old_mrf_config(),
:ok <- check_media_proxy_whitelist_config(),
:ok <- check_welcome_message_config(),
:ok <- check_gun_pool_options(),
:ok <- check_activity_expiration_config(),
:ok <- check_remote_ip_plug_name(),
:ok <- check_uploders_s3_public_endpoint(),
:ok <- check_old_chat_shoutbox() do
:ok
else
_ ->
:error
end
[
check_hellthread_threshold(),
check_old_mrf_config(),
check_media_proxy_whitelist_config(),
check_welcome_message_config(),
check_gun_pool_options(),
check_activity_expiration_config(),
check_remote_ip_plug_name(),
check_uploaders_s3_public_endpoint(),
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
check_simple_policy_tuples(),
check_exiftool_filter()
]
|> Enum.reduce(:ok, fn
:ok, :ok -> :ok
_, _ -> :error
end)
end
def check_welcome_message_config do
@ -80,7 +256,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
@ -98,17 +274,17 @@ defmodule Pleroma.Config.DeprecationWarnings do
if warning == "" do
:ok
else
Logger.warn(warning_preface <> warning)
Logger.warning(warning_preface <> warning)
:error
end
end
@spec check_media_proxy_whitelist_config() :: :ok | nil
@spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
Logger.warn("""
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
""")
@ -123,7 +299,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
pool_config = Config.get(:connections_pool)
if timeout = pool_config[:await_up_timeout] do
Logger.warn("""
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using old setting `config :pleroma, :connections_pool, await_up_timeout`. Please change to `config :pleroma, :connections_pool, connect_timeout` to ensure compatibility with future releases.
""")
@ -135,7 +311,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
warning_preface = """
!!!DEPRECATION WARNING!!!
Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. The setting will not take effect until updated.
"""
updated_config =
@ -155,7 +331,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `:timeout` options in #{pool_name} pool is now `:recv_timeout`"
end)
Logger.warn(Enum.join([warning_preface | pool_warnings]))
Logger.warning(Enum.join([warning_preface | pool_warnings]))
Config.put(:pools, updated_config)
:error
@ -164,7 +340,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
@spec check_activity_expiration_config() :: :ok | nil
@spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
@ -180,7 +356,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
@spec check_remote_ip_plug_name() :: :ok | nil
@spec check_remote_ip_plug_name() :: :ok | :error
def check_remote_ip_plug_name do
warning_preface = """
!!!DEPRECATION WARNING!!!
@ -196,8 +372,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
@spec check_uploders_s3_public_endpoint() :: :ok | nil
def check_uploders_s3_public_endpoint do
@spec check_uploaders_s3_public_endpoint() :: :ok | :error
def check_uploaders_s3_public_endpoint do
s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3])
use_old_config = Keyword.has_key?(s3_config, :public_endpoint)
@ -217,7 +393,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
@spec check_old_chat_shoutbox() :: :ok | nil
@spec check_old_chat_shoutbox() :: :ok | :error
def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || []

View file

@ -1,8 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Getting do
@callback get(any()) :: any()
@callback get(any(), any()) :: any()
def get(key), do: get(key, nil)
def get(key, default), do: impl().get(key, default)
def impl do
Application.get_env(:pleroma, :config_impl, Pleroma.Config)
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Helpers do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Holder do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
@ -19,21 +19,10 @@ defmodule Pleroma.Config.Loader do
:tesla
]
if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader
def read(path), do: @reader.read!(path)
else
# support for Elixir less than 1.9
@reader Mix.Config
def read(path) do
path
|> @reader.eval!()
|> elem(0)
end
end
@reader Config.Reader
@spec read(Path.t()) :: keyword()
def read(path), do: @reader.read!(path)
@spec merge(keyword(), keyword()) :: keyword()
def merge(c1, c2), do: @reader.merge(c1, c2)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Oban do
@ -21,11 +21,9 @@ defmodule Pleroma.Config.Oban do
"""
!!!OBAN CONFIG WARNING!!!
You are using old workers in Oban crontab settings, which were removed.
Please, remove setting from crontab in your config file (prod.secret.exs): #{
inspect(setting)
}
Please, remove setting from crontab in your config file (prod.secret.exs): #{inspect(setting)}
"""
|> Logger.warn()
|> Logger.warning()
List.delete(acc, setting)
else

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.ReleaseRuntimeProvider do
@moduledoc """
Imports runtime config and `{env}.exported_from_db.secret.exs` for releases.
@ -16,6 +20,20 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
with_runtime_config =
if File.exists?(config_path) do
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
%File.Stat{mode: mode} = File.stat!(config_path)
if Bitwise.band(mode, 0o007) > 0 do
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"
end
if Bitwise.band(mode, 0o020) > 0 do
raise "Configuration at #{config_path} has group-wise write permissions, execute the following: chmod g-w #{config_path}"
end
# Note: Elixir doesn't provides a getuid(2)
# so cannot forbid group-read only when config is owned by us
runtime_config = Config.Reader.read!(config_path)
with_defaults

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTask do
@ -47,7 +47,7 @@ defmodule Pleroma.Config.TransferTask do
{logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&merge_with_default/1)
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger] end)
logger
|> Enum.sort()
@ -55,8 +55,7 @@ defmodule Pleroma.Config.TransferTask do
started_applications = Application.started_applications()
# TODO: some problem with prometheus after restart!
reject = [nil, :prometheus, :postgrex]
reject = [nil, :postgrex]
reject =
if restart_pleroma? do
@ -104,11 +103,6 @@ defmodule Pleroma.Config.TransferTask do
end
# change logger configuration in runtime, without restart
defp configure({:quack, key, _, merged}) do
Logger.configure_backend(Quack.Logger, [{key, merged}])
:ok = update_env(:quack, key, merged)
end
defp configure({_, :backends, _, merged}) do
# removing current backends
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
@ -148,11 +142,9 @@ defmodule Pleroma.Config.TransferTask do
rescue
error ->
error_msg =
"updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
inspect(value)
} error: #{inspect(error)}"
"updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{inspect(value)} error: #{inspect(error)}"
Logger.warn(error_msg)
Logger.warning(error_msg)
nil
end
@ -186,12 +178,12 @@ defmodule Pleroma.Config.TransferTask do
:ok = Application.start(app)
else
nil ->
Logger.warn("#{app} is not started.")
Logger.warning("#{app} is not started.")
error ->
error
|> inspect()
|> Logger.warn()
|> Logger.warning()
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigDB do
@ -54,7 +54,7 @@ defmodule Pleroma.ConfigDB do
@spec get_by_params(map()) :: ConfigDB.t() | nil
def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
@spec changeset(ConfigDB.t(), map()) :: Ecto.Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
@ -138,7 +138,7 @@ defmodule Pleroma.ConfigDB do
end
end
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def update_or_create(params) do
params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
@ -163,7 +163,6 @@ defmodule Pleroma.ConfigDB do
defp only_full_update?(%ConfigDB{group: group, key: key}) do
full_key_update = [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:swarm, :node_blacklist},
@ -176,7 +175,7 @@ defmodule Pleroma.ConfigDB do
end)
end
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
def delete(params) do
@ -386,7 +385,7 @@ defmodule Pleroma.ConfigDB do
@spec module_name?(String.t()) :: boolean()
def module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Ueberauth|Swoosh)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Constants do
@ -19,7 +19,8 @@ defmodule Pleroma.Constants do
"context_id",
"deleted_activity_id",
"pleroma_internal",
"generator"
"generator",
"rules"
]
)
@ -27,4 +28,83 @@ defmodule Pleroma.Constants do
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
const(status_updatable_fields,
do: [
"source",
"tag",
"updated",
"emoji",
"content",
"summary",
"sensitive",
"attachment",
"generator"
]
)
const(status_object_types,
do: [
"Note",
"Question",
"Audio",
"Video",
"Event",
"Article",
"Page"
]
)
const(updatable_object_types,
do: [
"Note",
"Question",
"Audio",
"Video",
"Event",
"Article",
"Page"
]
)
const(actor_types,
do: [
"Application",
"Group",
"Organization",
"Person",
"Service"
]
)
const(allowed_user_actor_types,
do: [
"Person",
"Service",
"Group"
]
)
# basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex,
do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
)
const(upload_object_types, do: ["Document", "Image"])
const(activity_json_canonical_mime_type,
do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
const(activity_json_mime_types,
do: [
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"application/activity+json"
]
)
const(public_streams,
do: ["public", "public:local", "public:media", "public:local:media"]
)
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation do
@ -57,7 +57,7 @@ defmodule Pleroma.Conversation do
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
with true <- Pleroma.Web.ActivityPub.Visibility.direct?(activity),
"Create" <- activity.data["type"],
%Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"],

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation.RecipientShip do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CounterCache do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DataMigration do
@ -12,6 +12,8 @@ defmodule Pleroma.DataMigration do
import Ecto.Changeset
import Ecto.Query
@type t :: %__MODULE__{}
schema "data_migrations" do
field(:name, :string)
field(:state, State, default: :pending)
@ -42,4 +44,5 @@ defmodule Pleroma.DataMigration do
end
def populate_hashtags_table, do: get_by_name("populate_hashtags_table")
def delete_context_objects, do: get_by_name("delete_context_objects")
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Delivery do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Generator do
@ -15,8 +15,10 @@ defmodule Pleroma.Docs.Generator do
:code.all_loaded()
|> Enum.filter(fn {module, _} ->
# This shouldn't be needed as all modules are expected to have module_info/1,
# but in test enviroments some transient modules `:elixir_compiler_XX`
# but in test environments some transient modules `:elixir_compiler_XX`
# are loaded for some reason (where XX is a random integer).
Code.ensure_loaded(module)
if function_exported?(module, :module_info, 1) do
module.module_info(:attributes)
|> Keyword.get_values(:behaviour)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.JSON do
@ -18,7 +18,7 @@ defmodule Pleroma.Docs.JSON do
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
@spec compiled_descriptions :: Map.t()
@spec compiled_descriptions :: map()
def compiled_descriptions do
:persistent_term.get(@term)
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Markdown do

View file

@ -0,0 +1,10 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Translator do
require Pleroma.Docs.Translator.Compiler
require Pleroma.Web.Gettext
@before_compile Pleroma.Docs.Translator.Compiler
end

View file

@ -0,0 +1,119 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Docs.Translator.Compiler do
@external_resource "config/description.exs"
@raw_config Pleroma.Config.Loader.read("config/description.exs")
@raw_descriptions @raw_config[:pleroma][:config_description]
defmacro __before_compile__(_env) do
strings =
__MODULE__.descriptions()
|> __MODULE__.extract_strings()
quote do
def placeholder do
unquote do
Enum.map(
strings,
fn {path, type, string} ->
ctxt = msgctxt_for(path, type)
quote do
Pleroma.Web.Gettext.dpgettext_noop(
"config_descriptions",
unquote(ctxt),
unquote(string)
)
end
end
)
end
end
end
end
def descriptions do
Pleroma.Web.ActivityPub.MRF.config_descriptions()
|> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
|> Pleroma.Docs.Generator.convert_to_strings()
end
def extract_strings(descriptions) do
descriptions
|> Enum.reduce(%{strings: [], path: []}, &process_item/2)
|> Map.get(:strings)
end
defp process_item(entity, acc) do
current_level =
acc
|> process_desc(entity)
|> process_label(entity)
process_children(entity, current_level)
end
defp process_desc(acc, %{description: desc} = item) do
%{
strings: [{acc.path ++ [key_for(item)], "description", desc} | acc.strings],
path: acc.path
}
end
defp process_desc(acc, _) do
acc
end
defp process_label(acc, %{label: label} = item) do
%{
strings: [{acc.path ++ [key_for(item)], "label", label} | acc.strings],
path: acc.path
}
end
defp process_label(acc, _) do
acc
end
defp process_children(%{children: children} = item, acc) do
current_level = Map.put(acc, :path, acc.path ++ [key_for(item)])
children
|> Enum.reduce(current_level, &process_item/2)
|> Map.put(:path, acc.path)
end
defp process_children(_, acc) do
acc
end
def msgctxt_for(path, type) do
"config #{type} at #{Enum.join(path, " > ")}"
end
defp convert_group({_, group}) do
group
end
defp convert_group(group) do
group
end
def key_for(%{group: group, key: key}) do
"#{convert_group(group)}-#{key}"
end
def key_for(%{group: group}) do
convert_group(group)
end
def key_for(%{key: key}) do
key
end
def key_for(_) do
nil
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
import EctoEnum
@ -9,7 +9,9 @@ defenum(Pleroma.UserRelationship.Type,
mute: 2,
reblog_mute: 3,
notification_mute: 4,
inverse_subscription: 5
inverse_subscription: 5,
suggestion_dismiss: 6,
endorsement: 7
)
defenum(Pleroma.FollowingRelationship.State,
@ -25,3 +27,11 @@ defenum(Pleroma.DataMigration.State,
failed: 4,
manual: 5
)
defenum(Pleroma.User.Backup.State,
pending: 1,
running: 2,
complete: 3,
failed: 4,
invalid: 5
)

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri do
use Ecto.Type
def type, do: :string
def cast(uri) when is_binary(uri) do
parsed = URI.parse(uri)
if is_nil(parsed.scheme) do
:error
else
{:ok, uri}
end
end
def cast(_), do: :error
def dump(data), do: {:ok, data}
def load(data), do: {:ok, data}
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Emoji do

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.MIME do
use Ecto.Type
require Pleroma.Constants
def type, do: :string
def cast(mime) when is_binary(mime) do
if mime =~ Pleroma.Constants.mime_regex() do
{:ok, mime}
else
{:ok, "application/octet-stream"}
end
end
def cast(_), do: :error
def dump(data), do: {:ok, data}
def load(data), do: {:ok, data}
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.SafeText do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Uri do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.Config.Atom do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.Config.BinaryValue do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.AdminEmail do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.Mailer do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.NewUsersDigestEmail do

View file

@ -1,13 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails"
require Pleroma.Web.Gettext
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Gettext
alias Pleroma.Web.Router
import Swoosh.Email
@ -27,29 +30,75 @@ defmodule Pleroma.Emails.UserEmail do
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
def welcome(user, opts \\ %{}) do
new()
|> to(recipient(user))
|> from(Map.get(opts, :sender, sender()))
|> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
|> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
|> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
Gettext.with_locale_or_default user.language do
new()
|> to(recipient(user))
|> from(Map.get(opts, :sender, sender()))
|> subject(
Map.get(
opts,
:subject,
Gettext.dpgettext(
"static_pages",
"welcome email subject",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
|> html_body(
Map.get(
opts,
:html,
Gettext.dpgettext(
"static_pages",
"welcome email html body",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
|> text_body(
Map.get(
opts,
:text,
Gettext.dpgettext(
"static_pages",
"welcome email text body",
"Welcome to %{instance_name}!",
instance_name: instance_name()
)
)
)
end
end
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
Gettext.with_locale_or_default user.language do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
html_body = """
<h3>Reset your password at #{instance_name()}</h3>
<p>Someone has requested password change for your account at #{instance_name()}.</p>
<p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
"""
html_body =
Gettext.dpgettext(
"static_pages",
"password reset email body",
"""
<h3>Reset your password at %{instance_name}</h3>
<p>Someone has requested password change for your account at %{instance_name}.</p>
<p>If it was you, visit the following link to proceed: <a href="%{password_reset_url}">reset password</a>.</p>
<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
""",
instance_name: instance_name(),
password_reset_url: password_reset_url
)
new()
|> to(recipient(user))
|> from(sender())
|> subject("Password reset")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext("static_pages", "password reset email subject", "Password reset")
)
|> html_body(html_body)
end
end
def user_invitation_email(
@ -58,73 +107,136 @@ defmodule Pleroma.Emails.UserEmail do
to_email,
to_name \\ nil
) do
registration_url =
Router.Helpers.redirect_url(
Endpoint,
:registration_page,
user_invite_token.token
Gettext.with_locale_or_default user.language do
registration_url =
Router.Helpers.redirect_url(
Endpoint,
:registration_page,
user_invite_token.token
)
html_body =
Gettext.dpgettext(
"static_pages",
"user invitation email body",
"""
<h3>You are invited to %{instance_name}</h3>
<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>
<p>Click the following link to register: <a href="%{registration_url}">accept invitation</a>.</p>
""",
instance_name: instance_name(),
inviter_name: user.name,
registration_url: registration_url
)
new()
|> to(recipient(to_email, to_name))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"user invitation email subject",
"Invitation to %{instance_name}",
instance_name: instance_name()
)
)
html_body = """
<h3>You are invited to #{instance_name()}</h3>
<p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
<p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
"""
new()
|> to(recipient(to_email, to_name))
|> from(sender())
|> subject("Invitation to #{instance_name()}")
|> html_body(html_body)
|> html_body(html_body)
end
end
def account_confirmation_email(user) do
confirmation_url =
Router.Helpers.confirm_email_url(
Endpoint,
:confirm_email,
user.id,
to_string(user.confirmation_token)
Gettext.with_locale_or_default user.language do
confirmation_url =
Router.Helpers.confirm_email_url(
Endpoint,
:confirm_email,
user.id,
to_string(user.confirmation_token)
)
html_body =
Gettext.dpgettext(
"static_pages",
"confirmation email body",
"""
<h3>Thank you for registering on %{instance_name}</h3>
<p>Email confirmation is required to activate the account.</p>
<p>Please click the following link to <a href="%{confirmation_url}">activate your account</a>.</p>
""",
instance_name: instance_name(),
confirmation_url: confirmation_url
)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"confirmation email subject",
"%{instance_name} account confirmation",
instance_name: instance_name()
)
)
html_body = """
<h3>Thank you for registering on #{instance_name()}</h3>
<p>Email confirmation is required to activate the account.</p>
<p>Please click the following link to <a href="#{confirmation_url}">activate your account</a>.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("#{instance_name()} account confirmation")
|> html_body(html_body)
|> html_body(html_body)
end
end
def approval_pending_email(user) do
html_body = """
<h3>Awaiting Approval</h3>
<p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
"""
Gettext.with_locale_or_default user.language do
html_body =
Gettext.dpgettext(
"static_pages",
"approval pending email body",
"""
<h3>Awaiting Approval</h3>
<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>
""",
instance_name: instance_name()
)
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your account is awaiting approval")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"approval pending email subject",
"Your account is awaiting approval"
)
)
|> html_body(html_body)
end
end
def successful_registration_email(user) do
html_body = """
<h3>Hello @#{user.nickname},</h3>
<p>Your account at #{instance_name()} has been registered successfully.</p>
<p>No further action is required to activate your account.</p>
"""
Gettext.with_locale_or_default user.language do
html_body =
Gettext.dpgettext(
"static_pages",
"successful registration email body",
"""
<h3>Hello @%{nickname},</h3>
<p>Your account at %{instance_name} has been registered successfully.</p>
<p>No further action is required to activate your account.</p>
""",
nickname: user.nickname,
instance_name: instance_name()
)
new()
|> to(recipient(user))
|> from(sender())
|> subject("Account registered on #{instance_name()}")
|> html_body(html_body)
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"successful registration email subject",
"Account registered on %{instance_name}",
instance_name: instance_name()
)
)
|> html_body(html_body)
end
end
@doc """
@ -134,69 +246,78 @@ defmodule Pleroma.Emails.UserEmail do
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
Gettext.with_locale_or_default user.language do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity, fetch: false)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity, fetch: false)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
from = User.get_by_ap_id(notification.activity.actor)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
from = User.get_by_ap_id(notification.activity.actor)
if not is_nil(from) do
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
if not is_nil(from) do
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor)
}
end
end)
|> Enum.filter(& &1)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
html_data = %{
instance: instance_name(),
user: user,
mentions: mentions,
followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
}
html_data = %{
instance: instance_name(),
user: user,
mentions: mentions,
followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
}
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"digest email subject",
"Your digest from %{instance_name}",
instance_name: instance_name()
)
)
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
end
end
end
@ -226,27 +347,47 @@ defmodule Pleroma.Emails.UserEmail do
def backup_is_ready_email(backup, admin_user_id \\ nil) do
%{user: user} = Pleroma.Repo.preload(backup, :user)
download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
html_body =
if is_nil(admin_user_id) do
"""
<p>You requested a full backup of your Pleroma account. It's ready for download:</p>
<p><a href="#{download_url}">#{download_url}</a></p>
"""
else
admin = Pleroma.Repo.get(User, admin_user_id)
Gettext.with_locale_or_default user.language do
download_url = Pleroma.Web.PleromaAPI.BackupView.download_url(backup)
"""
<p>Admin @#{admin.nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
<p><a href="#{download_url}">#{download_url}</a></p>
"""
end
html_body =
if is_nil(admin_user_id) do
Gettext.dpgettext(
"static_pages",
"account archive email body - self-requested",
"""
<p>You requested a full backup of your Pleroma account. It's ready for download:</p>
<p><a href="%{download_url}">%{download_url}</a></p>
""",
download_url: download_url
)
else
admin = Pleroma.Repo.get(User, admin_user_id)
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your account archive is ready")
|> html_body(html_body)
Gettext.dpgettext(
"static_pages",
"account archive email body - admin requested",
"""
<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>
<p><a href="%{download_url}">%{download_url}</a></p>
""",
admin_nickname: admin.nickname,
download_url: download_url
)
end
new()
|> to(recipient(user))
|> from(sender())
|> subject(
Gettext.dpgettext(
"static_pages",
"account archive email subject",
"Your account archive is ready"
)
)
|> html_body(html_body)
end
end
end

View file

@ -1,13 +1,13 @@
# emoji-test.txt
# Date: 2020-09-12, 22:19:50 GMT
# © 2020 Unicode®, Inc.
# Date: 2022-08-12, 20:24:39 GMT
# © 2022 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html
# For terms of use, see https://www.unicode.org/terms_of_use.html
#
# Emoji Keyboard/Display Test Data for UTS #51
# Version: 13.1
# Version: 15.0
#
# For documentation and usage, see http://www.unicode.org/reports/tr51
# For documentation and usage, see https://www.unicode.org/reports/tr51
#
# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed.
# Format: code points; status # emoji name
@ -43,6 +43,7 @@
1F602 ; fully-qualified # 😂 E0.6 face with tears of joy
1F642 ; fully-qualified # 🙂 E1.0 slightly smiling face
1F643 ; fully-qualified # 🙃 E1.0 upside-down face
1FAE0 ; fully-qualified # 🫠 E14.0 melting face
1F609 ; fully-qualified # 😉 E0.6 winking face
1F60A ; fully-qualified # 😊 E0.6 smiling face with smiling eyes
1F607 ; fully-qualified # 😇 E1.0 smiling face with halo
@ -68,10 +69,13 @@
1F911 ; fully-qualified # 🤑 E1.0 money-mouth face
# subgroup: face-hand
1F917 ; fully-qualified # 🤗 E1.0 hugging face
1F917 ; fully-qualified # 🤗 E1.0 smiling face with open hands
1F92D ; fully-qualified # 🤭 E5.0 face with hand over mouth
1FAE2 ; fully-qualified # 🫢 E14.0 face with open eyes and hand over mouth
1FAE3 ; fully-qualified # 🫣 E14.0 face with peeking eye
1F92B ; fully-qualified # 🤫 E5.0 shushing face
1F914 ; fully-qualified # 🤔 E1.0 thinking face
1FAE1 ; fully-qualified # 🫡 E14.0 saluting face
# subgroup: face-neutral-skeptical
1F910 ; fully-qualified # 🤐 E1.0 zipper-mouth face
@ -79,6 +83,7 @@
1F610 ; fully-qualified # 😐 E0.7 neutral face
1F611 ; fully-qualified # 😑 E1.0 expressionless face
1F636 ; fully-qualified # 😶 E1.0 face without mouth
1FAE5 ; fully-qualified # 🫥 E14.0 dotted line face
1F636 200D 1F32B FE0F ; fully-qualified # 😶‍🌫️ E13.1 face in clouds
1F636 200D 1F32B ; minimally-qualified # 😶‍🌫 E13.1 face in clouds
1F60F ; fully-qualified # 😏 E0.6 smirking face
@ -87,6 +92,7 @@
1F62C ; fully-qualified # 😬 E1.0 grimacing face
1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling
1F925 ; fully-qualified # 🤥 E3.0 lying face
1FAE8 ; fully-qualified # 🫨 E15.0 shaking face
# subgroup: face-sleepy
1F60C ; fully-qualified # 😌 E0.6 relieved face
@ -105,7 +111,7 @@
1F975 ; fully-qualified # 🥵 E11.0 hot face
1F976 ; fully-qualified # 🥶 E11.0 cold face
1F974 ; fully-qualified # 🥴 E11.0 woozy face
1F635 ; fully-qualified # 😵 E0.6 knocked-out face
1F635 ; fully-qualified # 😵 E0.6 face with crossed-out eyes
1F635 200D 1F4AB ; fully-qualified # 😵‍💫 E13.1 face with spiral eyes
1F92F ; fully-qualified # 🤯 E5.0 exploding head
@ -121,6 +127,7 @@
# subgroup: face-concerned
1F615 ; fully-qualified # 😕 E1.0 confused face
1FAE4 ; fully-qualified # 🫤 E14.0 face with diagonal mouth
1F61F ; fully-qualified # 😟 E1.0 worried face
1F641 ; fully-qualified # 🙁 E1.0 slightly frowning face
2639 FE0F ; fully-qualified # ☹️ E0.7 frowning face
@ -130,6 +137,7 @@
1F632 ; fully-qualified # 😲 E0.6 astonished face
1F633 ; fully-qualified # 😳 E0.6 flushed face
1F97A ; fully-qualified # 🥺 E11.0 pleading face
1F979 ; fully-qualified # 🥹 E14.0 face holding back tears
1F626 ; fully-qualified # 😦 E1.0 frowning face with open mouth
1F627 ; fully-qualified # 😧 E1.0 anguished face
1F628 ; fully-qualified # 😨 E0.6 fearful face
@ -148,7 +156,7 @@
# subgroup: face-negative
1F624 ; fully-qualified # 😤 E0.6 face with steam from nose
1F621 ; fully-qualified # 😡 E0.6 pouting face
1F621 ; fully-qualified # 😡 E0.6 enraged face
1F620 ; fully-qualified # 😠 E0.6 angry face
1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth
1F608 ; fully-qualified # 😈 E1.0 smiling face with horns
@ -183,8 +191,7 @@
1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey
1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey
# subgroup: emotion
1F48B ; fully-qualified # 💋 E0.6 kiss mark
# subgroup: heart
1F48C ; fully-qualified # 💌 E0.6 love letter
1F498 ; fully-qualified # 💘 E0.6 heart with arrow
1F49D ; fully-qualified # 💝 E0.6 heart with ribbon
@ -203,14 +210,20 @@
2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart
2764 FE0F ; fully-qualified # ❤️ E0.6 red heart
2764 ; unqualified # ❤ E0.6 red heart
1FA77 ; fully-qualified # 🩷 E15.0 pink heart
1F9E1 ; fully-qualified # 🧡 E5.0 orange heart
1F49B ; fully-qualified # 💛 E0.6 yellow heart
1F49A ; fully-qualified # 💚 E0.6 green heart
1F499 ; fully-qualified # 💙 E0.6 blue heart
1FA75 ; fully-qualified # 🩵 E15.0 light blue heart
1F49C ; fully-qualified # 💜 E0.6 purple heart
1F90E ; fully-qualified # 🤎 E12.0 brown heart
1F5A4 ; fully-qualified # 🖤 E3.0 black heart
1FA76 ; fully-qualified # 🩶 E15.0 grey heart
1F90D ; fully-qualified # 🤍 E12.0 white heart
# subgroup: emotion
1F48B ; fully-qualified # 💋 E0.6 kiss mark
1F4AF ; fully-qualified # 💯 E0.6 hundred points
1F4A2 ; fully-qualified # 💢 E0.6 anger symbol
1F4A5 ; fully-qualified # 💥 E0.6 collision
@ -219,21 +232,20 @@
1F4A8 ; fully-qualified # 💨 E0.6 dashing away
1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole
1F573 ; unqualified # 🕳 E0.7 hole
1F4A3 ; fully-qualified # 💣 E0.6 bomb
1F4AC ; fully-qualified # 💬 E0.6 speech balloon
1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble
1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble
1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 E2.0 eye in speech bubble
1F441 FE0F 200D 1F5E8 ; minimally-qualified # 👁️‍🗨 E2.0 eye in speech bubble
1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble
1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble
1F5E8 ; unqualified # 🗨 E2.0 left speech bubble
1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble
1F5EF ; unqualified # 🗯 E0.7 right anger bubble
1F4AD ; fully-qualified # 💭 E1.0 thought balloon
1F4A4 ; fully-qualified # 💤 E0.6 zzz
1F4A4 ; fully-qualified # 💤 E0.6 ZZZ
# Smileys & Emotion subtotal: 170
# Smileys & Emotion subtotal: 170 w/o modifiers
# Smileys & Emotion subtotal: 180
# Smileys & Emotion subtotal: 180 w/o modifiers
# group: People & Body
@ -269,6 +281,42 @@
1F596 1F3FD ; fully-qualified # 🖖🏽 E1.0 vulcan salute: medium skin tone
1F596 1F3FE ; fully-qualified # 🖖🏾 E1.0 vulcan salute: medium-dark skin tone
1F596 1F3FF ; fully-qualified # 🖖🏿 E1.0 vulcan salute: dark skin tone
1FAF1 ; fully-qualified # 🫱 E14.0 rightwards hand
1FAF1 1F3FB ; fully-qualified # 🫱🏻 E14.0 rightwards hand: light skin tone
1FAF1 1F3FC ; fully-qualified # 🫱🏼 E14.0 rightwards hand: medium-light skin tone
1FAF1 1F3FD ; fully-qualified # 🫱🏽 E14.0 rightwards hand: medium skin tone
1FAF1 1F3FE ; fully-qualified # 🫱🏾 E14.0 rightwards hand: medium-dark skin tone
1FAF1 1F3FF ; fully-qualified # 🫱🏿 E14.0 rightwards hand: dark skin tone
1FAF2 ; fully-qualified # 🫲 E14.0 leftwards hand
1FAF2 1F3FB ; fully-qualified # 🫲🏻 E14.0 leftwards hand: light skin tone
1FAF2 1F3FC ; fully-qualified # 🫲🏼 E14.0 leftwards hand: medium-light skin tone
1FAF2 1F3FD ; fully-qualified # 🫲🏽 E14.0 leftwards hand: medium skin tone
1FAF2 1F3FE ; fully-qualified # 🫲🏾 E14.0 leftwards hand: medium-dark skin tone
1FAF2 1F3FF ; fully-qualified # 🫲🏿 E14.0 leftwards hand: dark skin tone
1FAF3 ; fully-qualified # 🫳 E14.0 palm down hand
1FAF3 1F3FB ; fully-qualified # 🫳🏻 E14.0 palm down hand: light skin tone
1FAF3 1F3FC ; fully-qualified # 🫳🏼 E14.0 palm down hand: medium-light skin tone
1FAF3 1F3FD ; fully-qualified # 🫳🏽 E14.0 palm down hand: medium skin tone
1FAF3 1F3FE ; fully-qualified # 🫳🏾 E14.0 palm down hand: medium-dark skin tone
1FAF3 1F3FF ; fully-qualified # 🫳🏿 E14.0 palm down hand: dark skin tone
1FAF4 ; fully-qualified # 🫴 E14.0 palm up hand
1FAF4 1F3FB ; fully-qualified # 🫴🏻 E14.0 palm up hand: light skin tone
1FAF4 1F3FC ; fully-qualified # 🫴🏼 E14.0 palm up hand: medium-light skin tone
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
1FAF7 ; fully-qualified # 🫷 E15.0 leftwards pushing hand
1FAF7 1F3FB ; fully-qualified # 🫷🏻 E15.0 leftwards pushing hand: light skin tone
1FAF7 1F3FC ; fully-qualified # 🫷🏼 E15.0 leftwards pushing hand: medium-light skin tone
1FAF7 1F3FD ; fully-qualified # 🫷🏽 E15.0 leftwards pushing hand: medium skin tone
1FAF7 1F3FE ; fully-qualified # 🫷🏾 E15.0 leftwards pushing hand: medium-dark skin tone
1FAF7 1F3FF ; fully-qualified # 🫷🏿 E15.0 leftwards pushing hand: dark skin tone
1FAF8 ; fully-qualified # 🫸 E15.0 rightwards pushing hand
1FAF8 1F3FB ; fully-qualified # 🫸🏻 E15.0 rightwards pushing hand: light skin tone
1FAF8 1F3FC ; fully-qualified # 🫸🏼 E15.0 rightwards pushing hand: medium-light skin tone
1FAF8 1F3FD ; fully-qualified # 🫸🏽 E15.0 rightwards pushing hand: medium skin tone
1FAF8 1F3FE ; fully-qualified # 🫸🏾 E15.0 rightwards pushing hand: medium-dark skin tone
1FAF8 1F3FF ; fully-qualified # 🫸🏿 E15.0 rightwards pushing hand: dark skin tone
# subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand
@ -302,6 +350,12 @@
1F91E 1F3FD ; fully-qualified # 🤞🏽 E3.0 crossed fingers: medium skin tone
1F91E 1F3FE ; fully-qualified # 🤞🏾 E3.0 crossed fingers: medium-dark skin tone
1F91E 1F3FF ; fully-qualified # 🤞🏿 E3.0 crossed fingers: dark skin tone
1FAF0 ; fully-qualified # 🫰 E14.0 hand with index finger and thumb crossed
1FAF0 1F3FB ; fully-qualified # 🫰🏻 E14.0 hand with index finger and thumb crossed: light skin tone
1FAF0 1F3FC ; fully-qualified # 🫰🏼 E14.0 hand with index finger and thumb crossed: medium-light skin tone
1FAF0 1F3FD ; fully-qualified # 🫰🏽 E14.0 hand with index finger and thumb crossed: medium skin tone
1FAF0 1F3FE ; fully-qualified # 🫰🏾 E14.0 hand with index finger and thumb crossed: medium-dark skin tone
1FAF0 1F3FF ; fully-qualified # 🫰🏿 E14.0 hand with index finger and thumb crossed: dark skin tone
1F91F ; fully-qualified # 🤟 E5.0 love-you gesture
1F91F 1F3FB ; fully-qualified # 🤟🏻 E5.0 love-you gesture: light skin tone
1F91F 1F3FC ; fully-qualified # 🤟🏼 E5.0 love-you gesture: medium-light skin tone
@ -359,6 +413,12 @@
261D 1F3FD ; fully-qualified # ☝🏽 E1.0 index pointing up: medium skin tone
261D 1F3FE ; fully-qualified # ☝🏾 E1.0 index pointing up: medium-dark skin tone
261D 1F3FF ; fully-qualified # ☝🏿 E1.0 index pointing up: dark skin tone
1FAF5 ; fully-qualified # 🫵 E14.0 index pointing at the viewer
1FAF5 1F3FB ; fully-qualified # 🫵🏻 E14.0 index pointing at the viewer: light skin tone
1FAF5 1F3FC ; fully-qualified # 🫵🏼 E14.0 index pointing at the viewer: medium-light skin tone
1FAF5 1F3FD ; fully-qualified # 🫵🏽 E14.0 index pointing at the viewer: medium skin tone
1FAF5 1F3FE ; fully-qualified # 🫵🏾 E14.0 index pointing at the viewer: medium-dark skin tone
1FAF5 1F3FF ; fully-qualified # 🫵🏿 E14.0 index pointing at the viewer: dark skin tone
# subgroup: hand-fingers-closed
1F44D ; fully-qualified # 👍 E0.6 thumbs up
@ -411,6 +471,12 @@
1F64C 1F3FD ; fully-qualified # 🙌🏽 E1.0 raising hands: medium skin tone
1F64C 1F3FE ; fully-qualified # 🙌🏾 E1.0 raising hands: medium-dark skin tone
1F64C 1F3FF ; fully-qualified # 🙌🏿 E1.0 raising hands: dark skin tone
1FAF6 ; fully-qualified # 🫶 E14.0 heart hands
1FAF6 1F3FB ; fully-qualified # 🫶🏻 E14.0 heart hands: light skin tone
1FAF6 1F3FC ; fully-qualified # 🫶🏼 E14.0 heart hands: medium-light skin tone
1FAF6 1F3FD ; fully-qualified # 🫶🏽 E14.0 heart hands: medium skin tone
1FAF6 1F3FE ; fully-qualified # 🫶🏾 E14.0 heart hands: medium-dark skin tone
1FAF6 1F3FF ; fully-qualified # 🫶🏿 E14.0 heart hands: dark skin tone
1F450 ; fully-qualified # 👐 E0.6 open hands
1F450 1F3FB ; fully-qualified # 👐🏻 E1.0 open hands: light skin tone
1F450 1F3FC ; fully-qualified # 👐🏼 E1.0 open hands: medium-light skin tone
@ -424,6 +490,31 @@
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake
1F91D 1F3FB ; fully-qualified # 🤝🏻 E14.0 handshake: light skin tone
1F91D 1F3FC ; fully-qualified # 🤝🏼 E14.0 handshake: medium-light skin tone
1F91D 1F3FD ; fully-qualified # 🤝🏽 E14.0 handshake: medium skin tone
1F91D 1F3FE ; fully-qualified # 🤝🏾 E14.0 handshake: medium-dark skin tone
1F91D 1F3FF ; fully-qualified # 🤝🏿 E14.0 handshake: dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏻‍🫲🏿 E14.0 handshake: light skin tone, dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏼‍🫲🏻 E14.0 handshake: medium-light skin tone, light skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏼‍🫲🏽 E14.0 handshake: medium-light skin tone, medium skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏼‍🫲🏾 E14.0 handshake: medium-light skin tone, medium-dark skin tone
1FAF1 1F3FC 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏼‍🫲🏿 E14.0 handshake: medium-light skin tone, dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏽‍🫲🏻 E14.0 handshake: medium skin tone, light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏽‍🫲🏼 E14.0 handshake: medium skin tone, medium-light skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏽‍🫲🏾 E14.0 handshake: medium skin tone, medium-dark skin tone
1FAF1 1F3FD 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏽‍🫲🏿 E14.0 handshake: medium skin tone, dark skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏾‍🫲🏻 E14.0 handshake: medium-dark skin tone, light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏾‍🫲🏼 E14.0 handshake: medium-dark skin tone, medium-light skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏾‍🫲🏽 E14.0 handshake: medium-dark skin tone, medium skin tone
1FAF1 1F3FE 200D 1FAF2 1F3FF ; fully-qualified # 🫱🏾‍🫲🏿 E14.0 handshake: medium-dark skin tone, dark skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FB ; fully-qualified # 🫱🏿‍🫲🏻 E14.0 handshake: dark skin tone, light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏿‍🫲🏼 E14.0 handshake: dark skin tone, medium-light skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏿‍🫲🏽 E14.0 handshake: dark skin tone, medium skin tone
1FAF1 1F3FF 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏿‍🫲🏾 E14.0 handshake: dark skin tone, medium-dark skin tone
1F64F ; fully-qualified # 🙏 E0.6 folded hands
1F64F 1F3FB ; fully-qualified # 🙏🏻 E1.0 folded hands: light skin tone
1F64F 1F3FC ; fully-qualified # 🙏🏼 E1.0 folded hands: medium-light skin tone
@ -501,6 +592,7 @@
1F441 ; unqualified # 👁 E0.7 eye
1F445 ; fully-qualified # 👅 E0.6 tongue
1F444 ; fully-qualified # 👄 E0.6 mouth
1FAE6 ; fully-qualified # 🫦 E14.0 biting lip
# subgroup: person
1F476 ; fully-qualified # 👶 E0.6 baby
@ -1380,7 +1472,7 @@
1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone
1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective
1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective
1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ E4.0 man detective
1F575 FE0F 200D 2642 ; minimally-qualified # 🕵️‍♂ E4.0 man detective
1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective
1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone
1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone
@ -1394,7 +1486,7 @@
1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone
1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective
1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective
1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ E4.0 woman detective
1F575 FE0F 200D 2640 ; minimally-qualified # 🕵️‍♀ E4.0 woman detective
1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective
1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone
1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone
@ -1472,6 +1564,12 @@
1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ E4.0 woman construction worker: medium-dark skin tone
1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ E4.0 woman construction worker: dark skin tone
1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ E4.0 woman construction worker: dark skin tone
1FAC5 ; fully-qualified # 🫅 E14.0 person with crown
1FAC5 1F3FB ; fully-qualified # 🫅🏻 E14.0 person with crown: light skin tone
1FAC5 1F3FC ; fully-qualified # 🫅🏼 E14.0 person with crown: medium-light skin tone
1FAC5 1F3FD ; fully-qualified # 🫅🏽 E14.0 person with crown: medium skin tone
1FAC5 1F3FE ; fully-qualified # 🫅🏾 E14.0 person with crown: medium-dark skin tone
1FAC5 1F3FF ; fully-qualified # 🫅🏿 E14.0 person with crown: dark skin tone
1F934 ; fully-qualified # 🤴 E3.0 prince
1F934 1F3FB ; fully-qualified # 🤴🏻 E3.0 prince: light skin tone
1F934 1F3FC ; fully-qualified # 🤴🏼 E3.0 prince: medium-light skin tone
@ -1592,6 +1690,18 @@
1F930 1F3FD ; fully-qualified # 🤰🏽 E3.0 pregnant woman: medium skin tone
1F930 1F3FE ; fully-qualified # 🤰🏾 E3.0 pregnant woman: medium-dark skin tone
1F930 1F3FF ; fully-qualified # 🤰🏿 E3.0 pregnant woman: dark skin tone
1FAC3 ; fully-qualified # 🫃 E14.0 pregnant man
1FAC3 1F3FB ; fully-qualified # 🫃🏻 E14.0 pregnant man: light skin tone
1FAC3 1F3FC ; fully-qualified # 🫃🏼 E14.0 pregnant man: medium-light skin tone
1FAC3 1F3FD ; fully-qualified # 🫃🏽 E14.0 pregnant man: medium skin tone
1FAC3 1F3FE ; fully-qualified # 🫃🏾 E14.0 pregnant man: medium-dark skin tone
1FAC3 1F3FF ; fully-qualified # 🫃🏿 E14.0 pregnant man: dark skin tone
1FAC4 ; fully-qualified # 🫄 E14.0 pregnant person
1FAC4 1F3FB ; fully-qualified # 🫄🏻 E14.0 pregnant person: light skin tone
1FAC4 1F3FC ; fully-qualified # 🫄🏼 E14.0 pregnant person: medium-light skin tone
1FAC4 1F3FD ; fully-qualified # 🫄🏽 E14.0 pregnant person: medium skin tone
1FAC4 1F3FE ; fully-qualified # 🫄🏾 E14.0 pregnant person: medium-dark skin tone
1FAC4 1F3FF ; fully-qualified # 🫄🏿 E14.0 pregnant person: dark skin tone
1F931 ; fully-qualified # 🤱 E5.0 breast-feeding
1F931 1F3FB ; fully-qualified # 🤱🏻 E5.0 breast-feeding: light skin tone
1F931 1F3FC ; fully-qualified # 🤱🏼 E5.0 breast-feeding: medium-light skin tone
@ -1862,6 +1972,7 @@
1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ E5.0 man zombie
1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ E5.0 woman zombie
1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ E5.0 woman zombie
1F9CC ; fully-qualified # 🧌 E14.0 troll
# subgroup: person-activity
1F486 ; fully-qualified # 💆 E0.6 person getting massage
@ -2208,7 +2319,7 @@
1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone
1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing
1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing
1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ E4.0 man golfing
1F3CC FE0F 200D 2642 ; minimally-qualified # 🏌️‍♂ E4.0 man golfing
1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing
1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone
1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone
@ -2222,7 +2333,7 @@
1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone
1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing
1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing
1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ E4.0 woman golfing
1F3CC FE0F 200D 2640 ; minimally-qualified # 🏌️‍♀ E4.0 woman golfing
1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing
1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone
1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone
@ -2333,7 +2444,7 @@
26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone
26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball
26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball
26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ E4.0 man bouncing ball
26F9 FE0F 200D 2642 ; minimally-qualified # ⛹️‍♂ E4.0 man bouncing ball
26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball
26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone
26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone
@ -2347,7 +2458,7 @@
26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone
26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball
26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball
26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ E4.0 woman bouncing ball
26F9 FE0F 200D 2640 ; minimally-qualified # ⛹️‍♀ E4.0 woman bouncing ball
26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball
26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone
26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone
@ -2368,7 +2479,7 @@
1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone
1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights
1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights
1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ E4.0 man lifting weights
1F3CB FE0F 200D 2642 ; minimally-qualified # 🏋️‍♂ E4.0 man lifting weights
1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights
1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone
1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone
@ -2382,7 +2493,7 @@
1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone
1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights
1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights
1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ E4.0 woman lifting weights
1F3CB FE0F 200D 2640 ; minimally-qualified # 🏋️‍♀ E4.0 woman lifting weights
1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights
1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone
1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone
@ -3168,8 +3279,8 @@
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints
# People & Body subtotal: 2899
# People & Body subtotal: 494 w/o modifiers
# People & Body subtotal: 2998
# People & Body subtotal: 508 w/o modifiers
# group: Component
@ -3212,6 +3323,8 @@
1F405 ; fully-qualified # 🐅 E1.0 tiger
1F406 ; fully-qualified # 🐆 E1.0 leopard
1F434 ; fully-qualified # 🐴 E0.6 horse face
1FACE ; fully-qualified # 🫎 E15.0 moose
1FACF ; fully-qualified # 🫏 E15.0 donkey
1F40E ; fully-qualified # 🐎 E0.6 horse
1F984 ; fully-qualified # 🦄 E1.0 unicorn
1F993 ; fully-qualified # 🦓 E5.0 zebra
@ -3279,6 +3392,9 @@
1F9A9 ; fully-qualified # 🦩 E12.0 flamingo
1F99A ; fully-qualified # 🦚 E11.0 peacock
1F99C ; fully-qualified # 🦜 E11.0 parrot
1FABD ; fully-qualified # 🪽 E15.0 wing
1F426 200D 2B1B ; fully-qualified # 🐦‍⬛ E15.0 black bird
1FABF ; fully-qualified # 🪿 E15.0 goose
# subgroup: animal-amphibian
1F438 ; fully-qualified # 🐸 E0.6 frog
@ -3304,6 +3420,8 @@
1F988 ; fully-qualified # 🦈 E3.0 shark
1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell
1FAB8 ; fully-qualified # 🪸 E14.0 coral
1FABC ; fully-qualified # 🪼 E15.0 jellyfish
# subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail
@ -3329,6 +3447,7 @@
1F490 ; fully-qualified # 💐 E0.6 bouquet
1F338 ; fully-qualified # 🌸 E0.6 cherry blossom
1F4AE ; fully-qualified # 💮 E0.6 white flower
1FAB7 ; fully-qualified # 🪷 E14.0 lotus
1F3F5 FE0F ; fully-qualified # 🏵️ E0.7 rosette
1F3F5 ; unqualified # 🏵 E0.7 rosette
1F339 ; fully-qualified # 🌹 E0.6 rose
@ -3337,6 +3456,7 @@
1F33B ; fully-qualified # 🌻 E0.6 sunflower
1F33C ; fully-qualified # 🌼 E0.6 blossom
1F337 ; fully-qualified # 🌷 E0.6 tulip
1FABB ; fully-qualified # 🪻 E15.0 hyacinth
# subgroup: plant-other
1F331 ; fully-qualified # 🌱 E0.6 seedling
@ -3353,9 +3473,12 @@
1F341 ; fully-qualified # 🍁 E0.6 maple leaf
1F342 ; fully-qualified # 🍂 E0.6 fallen leaf
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
1F344 ; fully-qualified # 🍄 E0.6 mushroom
# Animals & Nature subtotal: 147
# Animals & Nature subtotal: 147 w/o modifiers
# Animals & Nature subtotal: 159
# Animals & Nature subtotal: 159 w/o modifiers
# group: Food & Drink
@ -3394,9 +3517,11 @@
1F966 ; fully-qualified # 🥦 E5.0 broccoli
1F9C4 ; fully-qualified # 🧄 E12.0 garlic
1F9C5 ; fully-qualified # 🧅 E12.0 onion
1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts
1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut
1FADA ; fully-qualified # 🫚 E15.0 ginger root
1FADB ; fully-qualified # 🫛 E15.0 pea pod
# subgroup: food-prepared
1F35E ; fully-qualified # 🍞 E0.6 bread
@ -3491,6 +3616,7 @@
1F37B ; fully-qualified # 🍻 E0.6 clinking beer mugs
1F942 ; fully-qualified # 🥂 E3.0 clinking glasses
1F943 ; fully-qualified # 🥃 E3.0 tumbler glass
1FAD7 ; fully-qualified # 🫗 E14.0 pouring liquid
1F964 ; fully-qualified # 🥤 E5.0 cup with straw
1F9CB ; fully-qualified # 🧋 E13.0 bubble tea
1F9C3 ; fully-qualified # 🧃 E12.0 beverage box
@ -3504,10 +3630,11 @@
1F374 ; fully-qualified # 🍴 E0.6 fork and knife
1F944 ; fully-qualified # 🥄 E3.0 spoon
1F52A ; fully-qualified # 🔪 E0.6 kitchen knife
1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora
# Food & Drink subtotal: 131
# Food & Drink subtotal: 131 w/o modifiers
# Food & Drink subtotal: 135
# Food & Drink subtotal: 135 w/o modifiers
# group: Travel & Places
@ -3597,6 +3724,7 @@
2668 FE0F ; fully-qualified # ♨️ E0.6 hot springs
2668 ; unqualified # ♨ E0.6 hot springs
1F3A0 ; fully-qualified # 🎠 E0.6 carousel horse
1F6DD ; fully-qualified # 🛝 E14.0 playground slide
1F3A1 ; fully-qualified # 🎡 E0.6 ferris wheel
1F3A2 ; fully-qualified # 🎢 E0.6 roller coaster
1F488 ; fully-qualified # 💈 E0.6 barber pole
@ -3652,6 +3780,7 @@
1F6E2 FE0F ; fully-qualified # 🛢️ E0.7 oil drum
1F6E2 ; unqualified # 🛢 E0.7 oil drum
26FD ; fully-qualified # ⛽ E0.6 fuel pump
1F6DE ; fully-qualified # 🛞 E14.0 wheel
1F6A8 ; fully-qualified # 🚨 E0.6 police car light
1F6A5 ; fully-qualified # 🚥 E0.6 horizontal traffic light
1F6A6 ; fully-qualified # 🚦 E1.0 vertical traffic light
@ -3660,6 +3789,7 @@
# subgroup: transport-water
2693 ; fully-qualified # ⚓ E0.6 anchor
1F6DF ; fully-qualified # 🛟 E14.0 ring buoy
26F5 ; fully-qualified # ⛵ E0.6 sailboat
1F6F6 ; fully-qualified # 🛶 E3.0 canoe
1F6A4 ; fully-qualified # 🚤 E0.6 speedboat
@ -3797,8 +3927,8 @@
1F4A7 ; fully-qualified # 💧 E0.6 droplet
1F30A ; fully-qualified # 🌊 E0.6 water wave
# Travel & Places subtotal: 264
# Travel & Places subtotal: 264 w/o modifiers
# Travel & Places subtotal: 267
# Travel & Places subtotal: 267 w/o modifiers
# group: Activities
@ -3870,10 +4000,10 @@
1F3AF ; fully-qualified # 🎯 E0.6 bullseye
1FA80 ; fully-qualified # 🪀 E12.0 yo-yo
1FA81 ; fully-qualified # 🪁 E12.0 kite
1F52B ; fully-qualified # 🔫 E0.6 water pistol
1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball
1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick
@ -3882,6 +4012,7 @@
1F9E9 ; fully-qualified # 🧩 E11.0 puzzle piece
1F9F8 ; fully-qualified # 🧸 E11.0 teddy bear
1FA85 ; fully-qualified # 🪅 E13.0 piñata
1FAA9 ; fully-qualified # 🪩 E14.0 mirror ball
1FA86 ; fully-qualified # 🪆 E13.0 nesting dolls
2660 FE0F ; fully-qualified # ♠️ E0.6 spade suit
2660 ; unqualified # ♠ E0.6 spade suit
@ -3907,8 +4038,8 @@
1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot
# Activities subtotal: 95
# Activities subtotal: 95 w/o modifiers
# Activities subtotal: 96
# Activities subtotal: 96 w/o modifiers
# group: Objects
@ -3934,6 +4065,7 @@
1FA73 ; fully-qualified # 🩳 E12.0 shorts
1F459 ; fully-qualified # 👙 E0.6 bikini
1F45A ; fully-qualified # 👚 E0.6 womans clothes
1FAAD ; fully-qualified # 🪭 E15.0 folding hand fan
1F45B ; fully-qualified # 👛 E0.6 purse
1F45C ; fully-qualified # 👜 E0.6 handbag
1F45D ; fully-qualified # 👝 E0.6 clutch bag
@ -3949,6 +4081,7 @@
1F461 ; fully-qualified # 👡 E0.6 womans sandal
1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
1F462 ; fully-qualified # 👢 E0.6 womans boot
1FAAE ; fully-qualified # 🪮 E15.0 hair pick
1F451 ; fully-qualified # 👑 E0.6 crown
1F452 ; fully-qualified # 👒 E0.6 womans hat
1F3A9 ; fully-qualified # 🎩 E0.6 top hat
@ -3997,6 +4130,8 @@
1FA95 ; fully-qualified # 🪕 E12.0 banjo
1F941 ; fully-qualified # 🥁 E3.0 drum
1FA98 ; fully-qualified # 🪘 E13.0 long drum
1FA87 ; fully-qualified # 🪇 E15.0 maracas
1FA88 ; fully-qualified # 🪈 E15.0 flute
# subgroup: phone
1F4F1 ; fully-qualified # 📱 E0.6 mobile phone
@ -4009,6 +4144,7 @@
# subgroup: computer
1F50B ; fully-qualified # 🔋 E0.6 battery
1FAAB ; fully-qualified # 🪫 E14.0 low battery
1F50C ; fully-qualified # 🔌 E0.6 electric plug
1F4BB ; fully-qualified # 💻 E0.6 laptop
1F5A5 FE0F ; fully-qualified # 🖥️ E0.7 desktop computer
@ -4168,7 +4304,7 @@
1F5E1 ; unqualified # 🗡 E0.7 dagger
2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords
2694 ; unqualified # ⚔ E1.0 crossed swords
1F52B ; fully-qualified # 🔫 E0.6 water pistol
1F4A3 ; fully-qualified # 💣 E0.6 bomb
1FA83 ; fully-qualified # 🪃 E13.0 boomerang
1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow
1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield
@ -4207,7 +4343,9 @@
1FA78 ; fully-qualified # 🩸 E12.0 drop of blood
1F48A ; fully-qualified # 💊 E0.6 pill
1FA79 ; fully-qualified # 🩹 E12.0 adhesive bandage
1FA7C ; fully-qualified # 🩼 E14.0 crutch
1FA7A ; fully-qualified # 🩺 E12.0 stethoscope
1FA7B ; fully-qualified # 🩻 E14.0 x-ray
# subgroup: household
1F6AA ; fully-qualified # 🚪 E0.6 door
@ -4232,6 +4370,7 @@
1F9FB ; fully-qualified # 🧻 E11.0 roll of paper
1FAA3 ; fully-qualified # 🪣 E13.0 bucket
1F9FC ; fully-qualified # 🧼 E11.0 soap
1FAE7 ; fully-qualified # 🫧 E14.0 bubbles
1FAA5 ; fully-qualified # 🪥 E13.0 toothbrush
1F9FD ; fully-qualified # 🧽 E11.0 sponge
1F9EF ; fully-qualified # 🧯 E11.0 fire extinguisher
@ -4244,11 +4383,14 @@
1FAA6 ; fully-qualified # 🪦 E13.0 headstone
26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn
26B1 ; unqualified # ⚱ E1.0 funeral urn
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard
1FAAA ; fully-qualified # 🪪 E14.0 identification card
# Objects subtotal: 299
# Objects subtotal: 299 w/o modifiers
# Objects subtotal: 310
# Objects subtotal: 310 w/o modifiers
# group: Symbols
@ -4344,6 +4486,7 @@
262E ; unqualified # ☮ E1.0 peace symbol
1F54E ; fully-qualified # 🕎 E1.0 menorah
1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star
1FAAF ; fully-qualified # 🪯 E15.0 khanda
# subgroup: zodiac
2648 ; fully-qualified # ♈ E0.6 Aries
@ -4392,6 +4535,7 @@
1F505 ; fully-qualified # 🔅 E1.0 dim button
1F506 ; fully-qualified # 🔆 E1.0 bright button
1F4F6 ; fully-qualified # 📶 E0.6 antenna bars
1F6DC ; fully-qualified # 🛜 E15.0 wireless
1F4F3 ; fully-qualified # 📳 E0.6 vibration mode
1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off
@ -4409,6 +4553,7 @@
2795 ; fully-qualified # E0.6 plus
2796 ; fully-qualified # E0.6 minus
2797 ; fully-qualified # ➗ E0.6 divide
1F7F0 ; fully-qualified # 🟰 E14.0 heavy equals sign
267E FE0F ; fully-qualified # ♾️ E11.0 infinity
267E ; unqualified # ♾ E11.0 infinity
@ -4581,8 +4726,8 @@
1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button
# Symbols subtotal: 301
# Symbols subtotal: 301 w/o modifiers
# Symbols subtotal: 304
# Symbols subtotal: 304 w/o modifiers
# group: Flags
@ -4597,7 +4742,7 @@
1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag
1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag
1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag
1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️‍⚧ E13.0 transgender flag
1F3F3 FE0F 200D 26A7 ; minimally-qualified # 🏳️‍⚧ E13.0 transgender flag
1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag
1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag
1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag
@ -4871,9 +5016,9 @@
# Flags subtotal: 275 w/o modifiers
# Status Counts
# fully-qualified : 3512
# minimally-qualified : 817
# unqualified : 252
# fully-qualified : 3655
# minimally-qualified : 827
# unqualified : 242
# component : 9
#EOF

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji do
@ -9,6 +9,7 @@ defmodule Pleroma.Emoji do
"""
use GenServer
alias Pleroma.Emoji.Combinations
alias Pleroma.Emoji.Loader
require Logger
@ -23,6 +24,8 @@ defmodule Pleroma.Emoji do
defstruct [:code, :file, :tags, :safe_code, :safe_file]
@type t :: %__MODULE__{}
@doc "Build emoji struct"
def build({code, file, tags}) do
%__MODULE__{
@ -48,10 +51,12 @@ defmodule Pleroma.Emoji do
end
@doc "Returns the path of the emoji `name`."
@spec get(String.t()) :: String.t() | nil
@spec get(String.t()) :: Pleroma.Emoji.t() | nil
def get(name) do
name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do
[{_, path}] -> path
[{_, emoji}] -> emoji
_ -> nil
end
end
@ -133,8 +138,72 @@ defmodule Pleroma.Emoji do
emojis = emojis ++ regional_indicators
for emoji <- emojis do
def is_unicode_emoji?(unquote(emoji)), do: true
def unicode?(unquote(emoji)), do: true
end
def is_unicode_emoji?(_), do: false
def unicode?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
def custom?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
def custom?(_), do: false
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
def maybe_strip_name(name), do: name
def maybe_quote(name) when is_binary(name) do
if unicode?(name) do
name
else
if String.starts_with?(name, ":") do
name
else
":#{name}:"
end
end
end
def maybe_quote(name), do: name
def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
emoji = maybe_strip_name(emoji)
tag =
tags
|> Enum.find(fn tag ->
tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["name"] == emoji
end)
if is_nil(tag) do
nil
else
tag
|> Map.get("icon")
|> Map.get("url")
end
end
def emoji_url(_), do: nil
def emoji_name_with_instance(name, url) do
url = url |> URI.parse() |> Map.get(:host)
"#{name}@#{url}"
end
emoji_qualification_map =
emojis
|> Enum.filter(&String.contains?(&1, "\uFE0F"))
|> Combinations.variate_emoji_qualification()
for {qualified, unqualified_list} <- emoji_qualification_map do
for unqualified <- unqualified_list do
def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
end
end
def fully_qualify_emoji(emoji), do: emoji
end

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Combinations do
# FE0F is the emoji variation sequence. It is used for fully-qualifying
# emoji, and that includes emoji combinations.
# This code generates combinations per emoji: for each FE0F, all possible
# combinations of the character being removed or staying will be generated.
# This is made as an attempt to find all partially-qualified and unqualified
# versions of a fully-qualified emoji.
# I have found *no cases* for which this would be a problem, after browsing
# the entire emoji list in emoji-test.txt. This is safe, and, sadly, most
# likely sane too.
defp qualification_combinations(codepoints) do
qualification_combinations([[]], codepoints)
end
defp qualification_combinations(acc, []), do: acc
defp qualification_combinations(acc, ["\uFE0F" | tail]) do
acc
|> Enum.flat_map(fn x -> [x, x ++ ["\uFE0F"]] end)
|> qualification_combinations(tail)
end
defp qualification_combinations(acc, [codepoint | tail]) do
acc
|> Enum.map(&Kernel.++(&1, [codepoint]))
|> qualification_combinations(tail)
end
def variate_emoji_qualification(emoji) when is_binary(emoji) do
emoji
|> String.codepoints()
|> qualification_combinations()
|> Enum.map(&List.to_string/1)
end
def variate_emoji_qualification(emoji) when is_list(emoji) do
emoji
|> Enum.map(fn emoji -> {emoji, variate_emoji_qualification(emoji)} end)
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Formatter do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Loader do
@ -15,8 +15,6 @@ defmodule Pleroma.Emoji.Loader do
require Logger
@mix_env Mix.env()
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@ -59,10 +57,8 @@ defmodule Pleroma.Emoji.Loader do
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
Logger.warning(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{Enum.join(files, ", ")}"
)
end
@ -81,7 +77,7 @@ defmodule Pleroma.Emoji.Loader do
# for testing emoji.txt entries we do not want exposed in normal operation
test_emoji =
if @mix_env == :test do
if Application.get_env(:pleroma, __MODULE__)[:test_emoji] do
load_from_file("test/config/emoji.txt", emoji_groups)
else
[]
@ -105,6 +101,7 @@ defmodule Pleroma.Emoji.Loader do
pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(pack_file) do
Logger.info("Loading emoji pack from JSON: #{pack_file}")
contents = Jason.decode!(File.read!(pack_file))
contents["files"]
@ -117,14 +114,13 @@ defmodule Pleroma.Emoji.Loader do
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}")
load_from_file(emoji_txt, emoji_groups)
else
extensions = Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
Enum.join(extensions, ", ")
} files are emoji"
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"
)
make_shortcode_to_file_map(pack_dir, extensions)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.Pack do
@ -100,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok, _emoji_files} =
:zip.unzip(
to_charlist(file.path),
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}]
)
{_, updated_pack} =
@ -209,7 +209,9 @@ defmodule Pleroma.Emoji.Pack do
with :ok <- validate_shareable_packs_available(uri) do
uri
|> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
|> URI.merge(
"/api/v1/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}"
)
|> http_get()
end
end
@ -249,8 +251,12 @@ defmodule Pleroma.Emoji.Pack do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri),
{:ok, %{"files_count" => files_count}} <-
uri |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=0") |> http_get(),
{:ok, remote_pack} <-
uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(),
uri
|> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=#{files_count}")
|> http_get(),
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
{:ok, archive} <- download_archive(url, sha),
pack <- copy_as(remote_pack, as || name),
@ -285,6 +291,7 @@ defmodule Pleroma.Emoji.Pack do
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
def load_pack(name) do
name = Path.basename(name)
pack_file = Path.join([emoji_path(), name, "pack.json"])
with {:ok, _} <- File.stat(pack_file),
@ -591,7 +598,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok,
%{
sha: sha,
url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
url: URI.merge(uri, "/api/v1/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Filter do
@ -216,9 +216,6 @@ defmodule Pleroma.Filter do
:re ->
~r/\b#{phrases}\b/i
_ ->
nil
end
end

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationship do
@ -194,12 +194,13 @@ defmodule Pleroma.FollowingRelationship do
|> join(:inner, [r], f in assoc(r, :follower))
|> where(following_id: ^origin.id)
|> where([r, f], f.allow_following_move == true)
|> where([r, f], f.local == true)
|> limit(50)
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
Repo.delete(following_relationship)
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
end)
|> case do
[] ->
@ -240,13 +241,13 @@ defmodule Pleroma.FollowingRelationship do
end
@doc """
For a query with joined activity,
keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
For a query with joined activity's actor,
keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
"""
def keep_following_or_not_domain_blocked(query, user) do
where(
query,
[_, activity],
[_, user_actor: user_actor],
fragment(
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
"""
@ -254,9 +255,9 @@ defmodule Pleroma.FollowingRelationship do
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
""",
activity.actor,
user_actor.ap_id,
^user.domain_blocks,
activity.actor,
user_actor.ap_id,
^User.binary_id(user.id),
^accept_state_code()
)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
@ -34,32 +34,34 @@ defmodule Pleroma.Formatter do
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
user_url = user.uri || user.ap_id
nickname_text = get_nickname_text(nickname, opts)
link =
Phoenix.HTML.Tag.content_tag(
:span,
Phoenix.HTML.Tag.content_tag(
:a,
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
href: user_url,
rel: "ugc"
),
class: "h-card"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
%User{} = user ->
{mention_from_user(user, opts),
%{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
_ ->
{buffer, acc}
end
end
def mention_from_user(%User{id: id} = user, opts \\ %{mentions_format: :full}) do
user_url = user.uri || user.ap_id
nickname_text = get_nickname_text(user.nickname, opts)
Phoenix.HTML.Tag.content_tag(
:span,
Phoenix.HTML.Tag.content_tag(
:a,
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
href: user_url,
rel: "ugc"
),
class: "h-card"
)
|> Phoenix.HTML.safe_to_string()
end
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.Endpoint.url()}/tag/#{tag}"
@ -122,7 +124,7 @@ defmodule Pleroma.Formatter do
end
def markdown_to_html(text) do
Earmark.as_html!(text, %Earmark.Options{compact_output: true})
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end
def html_escape({text, mentions, hashtags}, type) do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Frontend do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gopher.Server do
@ -114,7 +114,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/notices/" <> id) do
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.is_public?(activity) do
true <- Visibility.public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.API do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.Conn do
@ -56,10 +56,8 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
Logger.warn(
"Opening proxied connection to #{compose_uri_log(uri)} failed with error #{
inspect(error)
}"
Logger.warning(
"Opening proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)
error
@ -92,10 +90,8 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
Logger.warn(
"Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{
inspect(error)
}"
Logger.warning(
"Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)
error
@ -110,7 +106,7 @@ defmodule Pleroma.Gun.Conn do
{:ok, conn, protocol}
else
error ->
Logger.warn(
Logger.warning(
"Opening connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}"
)

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
@ -9,7 +9,7 @@ defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
def start_monitor do
pid =
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
case GenServer.start_link(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
{:ok, pid} ->
pid

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.Worker do

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
@ -18,10 +18,12 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
)
end
def start_worker(opts, retry \\ false) do
def start_worker(opts, last_attempt \\ false) do
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
{:error, :max_children} ->
if retry or free_pool() == :error do
funs = [fn -> last_attempt end, fn -> match?(:error, free_pool()) end]
if Enum.any?(funs, fn fun -> fun.() end) do
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
{:error, :pool_full}
else

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Hashtag do

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