Merge remote-tracking branch 'origin/develop' into review-pr-7933

This commit is contained in:
Lain Soykaf 2026-07-24 23:24:11 +04:00
commit 465a81ecf2
No known key found for this signature in database
115 changed files with 1733 additions and 546 deletions

View file

@ -252,40 +252,43 @@ defmodule Mix.Tasks.Pleroma.Config do
if whitelisted_configs in [nil, false] do
shell_error("No unwanted settings in ConfigDB. No changes made.")
else
whitelisted_groups =
whitelisted_configs
|> Enum.filter(fn
{_group} -> true
_ -> false
end)
|> Enum.map(fn {group} -> group end)
whitelisted_keys =
whitelisted_configs
|> Enum.filter(fn
{_group, _key} -> true
_ -> false
end)
{whitelisted_groups, whitelisted_keys} = get_filters(whitelisted_configs)
filtered =
from(c in ConfigDB)
|> Repo.all()
|> Enum.filter(&not_whitelisted?(&1, whitelisted_groups, whitelisted_keys))
if not Enum.empty?(filtered) do
shell_info("The following settings will be removed from ConfigDB:\n")
Enum.each(filtered, &dump(&1))
do_config_filter(filtered, force)
end
end
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
# Removes blacklisted configuration sections
def run(["filter_blacklisted" | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [force: :boolean],
aliases: [f: :force]
)
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
else
shell_error("No changes made.")
end
else
shell_error("No unwanted settings in ConfigDB. No changes made.")
end
force = Keyword.get(options, :force, false)
start_pleroma()
blacklisted_configs = Pleroma.Config.get(:database_config_blacklist)
if blacklisted_configs in [nil, false] do
shell_error("No unwanted settings in ConfigDB. No changes made.")
else
{blacklisted_groups, blacklisted_keys} = get_filters(blacklisted_configs)
filtered =
from(c in ConfigDB)
|> Repo.all()
|> Enum.filter(&blacklisted?(&1, blacklisted_groups, blacklisted_keys))
do_config_filter(filtered, force)
end
end
@ -490,8 +493,49 @@ defmodule Mix.Tasks.Pleroma.Config do
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
end
defp get_filters(config) do
groups =
config
|> Enum.filter(fn
{_group} -> true
_ -> false
end)
|> Enum.map(fn {group} -> group end)
keys =
config
|> Enum.filter(fn
{_group, _key} -> true
_ -> false
end)
{groups, keys}
end
defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do
not Enum.member?(whitelisted_groups, group) and
not Enum.member?(whitelisted_keys, {group, key})
end
defp blacklisted?(%{group: group, key: key}, blacklisted_groups, blacklisted_keys) do
Enum.member?(blacklisted_groups, group) or
Enum.member?(blacklisted_keys, {group, key})
end
defp do_config_filter(filtered, _) when length(filtered) == 0 do
shell_error("No unwanted settings in ConfigDB. No changes made.")
end
defp do_config_filter(filtered, force) do
shell_info("The following settings will be removed from ConfigDB:\n")
Enum.each(filtered, &dump(&1))
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
else
shell_error("No changes made.")
end
end
end

View file

@ -43,6 +43,7 @@ defmodule Pleroma.Application do
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
configure_logger()
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
@ -113,6 +114,44 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
def configure_logger do
Config.get([:logger, :backends], [])
|> Enum.each(fn backend ->
case backend_to_logger(backend) do
{:ok, logger} ->
add_logger(logger)
{:error, :console_backend} ->
Logger.warning(":console is no longer considered a backend and is enabled by default")
end
end)
end
defp add_logger(backend) do
case LoggerBackends.add(backend) do
{:ok, _} ->
Logger.debug("Successfully added logger backend: #{inspect(backend)}")
{:error, :already_present} ->
:ok
{:error, reason} ->
Logger.error("Failed to add logger backend #{inspect(backend)}: #{inspect(reason)}")
end
end
defp backend_to_logger(:console), do: {:error, :console_backend}
defp backend_to_logger({:ex_syslogger = backend, name}) do
Logger.warning(
"Configuration {:#{backend}, :#{name}} is incorrect. Use {ExSyslogger, :#{name}} instead!"
)
{:ok, {ExSyslogger, name}}
end
defp backend_to_logger(backend), do: {:ok, backend}
def load_custom_modules do
dir = Config.get([:modules, :runtime_dir])

View file

@ -108,4 +108,6 @@ defmodule Pleroma.Chat do
|> cast(%{pinned: false}, [:pinned])
|> Repo.update()
end
def enabled?, do: Pleroma.Config.get([__MODULE__, :enabled], true)
end

View file

@ -20,6 +20,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
]
@logger_formatter_config_knobs [:colors, :format, :metadata, :truncate, :utc_log]
def check_exiftool_filter do
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
@ -218,7 +220,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
check_simple_policy_tuples(),
check_exiftool_filter()
check_exiftool_filter(),
check_deprecated_logger_config()
]
|> Enum.reduce(:ok, fn
:ok, :ok -> :ok
@ -415,4 +418,72 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok
end
end
defp merge_deprecated_logger_config(config) do
formatter_config = Enum.filter(config, fn {k, _} -> k in @logger_formatter_config_knobs end)
formatter = Logger.default_formatter(formatter_config)
log_level = Keyword.get(config, :level, nil)
Logger.debug("""
Reconfiguring console Logger with deprecated configuration syntax.
Handler configuration:
log_level: #{inspect(log_level)}
Formatter:
#{inspect(formatter)}
""")
if Config.get(:env) != :test do
if log_level, do: :logger.update_handler_config(:default, :level, log_level)
:logger.set_handler_config(:default, :formatter, formatter)
end
end
@spec check_deprecated_logger_config() :: :ok | :error
def check_deprecated_logger_config do
backends_config = Application.get_env(:logger, :backends)
console_config = Application.get_env(:logger, :console)
# NOTE: No need to merge the old backends config since it still works.
# And new configuration will just add new Logger backends.
backend =
if backends_config do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
Please update your configuration at your earliest convenience to use:
`config :pleroma, :logger, backends: [...]`
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
`config :logger, :default_handler: false`
""")
true
else
false
end
console =
if console_config do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your configuration is using deprecated syntax for configuring logging to console.
`config :logger, :console` is deprecated syntax due to changes in Elixir.
Please update your configuration at your earliest convenience to use
`config :logger, default_handler` and `config :logger, :default_formatter`.
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
""")
merge_deprecated_logger_config(console_config)
true
else
false
end
if backend or console, do: :error, else: :ok
end
end

View file

@ -51,7 +51,7 @@ defmodule Pleroma.Config.TransferTask do
started_applications = Application.started_applications()
reject = [nil, :postgrex]
reject = [nil, :postgrex, :logger]
reject =
if restart_pleroma? do

View file

@ -12,13 +12,27 @@ defmodule Pleroma.Docs.Generator do
@spec list_behaviour_implementations(behaviour :: module()) :: [module()]
def list_behaviour_implementations(behaviour) do
:code.all_loaded()
|> Enum.filter(fn {module, _} ->
# :code.all_loaded() no longer works on Elixir 1.19 and newer since modules are lazily loaded.
# We can get a complete list of modules by interating over Application.loaded_applications/0,
# but that includes every module which would get loaded below, which is very slow.
# Hardcode the Pleroma application instead, which is good enough for now.
application_modules =
case :application.get_key(:pleroma, :modules) do
{:ok, modules} -> modules
:undefined -> []
end
# At compile time or in tests some modules will be outside of the Pleroma application.
loaded_modules = Enum.map(:code.all_loaded(), fn {module, _} -> module end)
(application_modules ++ loaded_modules)
|> Enum.uniq()
|> Enum.filter(fn module ->
Code.ensure_loaded(module)
# This shouldn't be needed as all modules are expected to have module_info/1,
# 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)
@ -26,7 +40,6 @@ defmodule Pleroma.Docs.Generator do
|> Enum.member?(behaviour)
end
end)
|> Enum.map(fn {module, _} -> module end)
end
@doc """

View file

@ -272,13 +272,13 @@ defmodule Pleroma.User do
end
def cached_blocked_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
@cachex.fetch!(:user_cache, cache_key(:blocked_users_ap_ids, user), fn _ ->
blocked_users_ap_ids(user)
end)
end
def cached_muted_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
@cachex.fetch!(:user_cache, cache_key(:muted_users_ap_ids, user), fn _ ->
muted_users_ap_ids(user)
end)
end
@ -1253,13 +1253,18 @@ defmodule Pleroma.User do
get_cached_by_nickname(nickname)
end
defp cache_key(:id, %User{id: id}), do: cache_key(:id, id)
defp cache_key(:nickname, %User{nickname: nickname}), do: cache_key(:nickname, nickname)
defp cache_key(namespace, %User{ap_id: ap_id}), do: cache_key(namespace, ap_id)
defp cache_key(namespace, value), do: "#{namespace}:#{value}"
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
@cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
@cachex.put(:user_cache, "nickname:#{user.nickname}", user)
@cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
@cachex.put(:user_cache, cache_key(:ap_id, user), user)
@cachex.put(:user_cache, cache_key(:nickname, user), user)
@cachex.put(:user_cache, cache_key(:friends_ap_ids, user), get_user_friends_ap_ids(user))
{:ok, user}
end
@ -1287,22 +1292,23 @@ defmodule Pleroma.User do
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
@cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
@cachex.fetch!(:user_cache, cache_key(:friends_ap_ids, user), fn _ ->
get_user_friends_ap_ids(user)
end)
end
def invalidate_cache(user) do
@cachex.del(:user_cache, "ap_id:#{user.ap_id}")
@cachex.del(:user_cache, "nickname:#{user.nickname}")
@cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, cache_key(:ap_id, user))
@cachex.del(:user_cache, cache_key(:nickname, user))
@cachex.del(:user_cache, cache_key(:id, user))
@cachex.del(:user_cache, cache_key(:friends_ap_ids, user))
@cachex.del(:user_cache, cache_key(:blocked_users_ap_ids, user))
@cachex.del(:user_cache, cache_key(:muted_users_ap_ids, user))
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
key = cache_key(:ap_id, ap_id)
with {:ok, nil} <- @cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
@ -1315,14 +1321,14 @@ defmodule Pleroma.User do
end
def get_cached_by_id(id) do
key = "id:#{id}"
key = cache_key(:id, id)
ap_id =
@cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
@cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
@cachex.put(:user_cache, cache_key(:ap_id, user), user)
{:commit, user.ap_id}
else
{:ignore, ""}
@ -1333,7 +1339,7 @@ defmodule Pleroma.User do
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
key = cache_key(:nickname, nickname)
@cachex.fetch!(:user_cache, key, fn _ ->
case get_or_fetch_by_nickname(nickname) do
@ -1651,7 +1657,7 @@ defmodule Pleroma.User do
|> Oban.insert()
end
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
@cachex.del(:user_cache, cache_key(:muted_users_ap_ids, muter))
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
@ -1661,7 +1667,7 @@ defmodule Pleroma.User do
with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(muter, mutee) do
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
@cachex.del(:user_cache, cache_key(:muted_users_ap_ids, muter))
{:ok, [user_mute, user_notification_mute]}
end
end
@ -2815,7 +2821,7 @@ defmodule Pleroma.User do
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked, expires_at) do
with {:ok, relationship} <- UserRelationship.create_block(user, blocked, expires_at) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, cache_key(:blocked_users_ap_ids, user))
{:ok, relationship}
end
end
@ -2824,7 +2830,7 @@ defmodule Pleroma.User do
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, cache_key(:blocked_users_ap_ids, user))
{:ok, relationship}
end
end

View file

@ -893,14 +893,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.all()
# Note: NO extra ordering should be done on "activities.id desc nulls last" for optimal plan
from(
[_activity, object] in query,
join: hto in "hashtags_objects",
on: hto.object_id == object.id,
where: hto.hashtag_id in ^hashtag_ids,
distinct: [desc: object.id],
order_by: [desc: object.id]
)
query =
from(
[_activity, object] in query,
join: hto in "hashtags_objects",
on: hto.object_id == object.id,
where: hto.hashtag_id in ^hashtag_ids
)
if is_nil(query.distinct) do
from([_activity, object] in query,
distinct: [desc: object.id],
order_by: [desc: object.id]
)
else
query
end
end
defp restrict_hashtag_any(query, %{tag: tag}) when is_binary(tag) do

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
@moduledoc "Adds expiration to all local Create activities"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(activity) do
activity =

View file

@ -3,11 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
@moduledoc "Prevent followbots from following with a bit of heuristic"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
@moduledoc "Prevent followbots from following with a bit of heuristic"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
# XXX: this should become User.normalize_by_ap_id() or similar, really.
defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id)

View file

@ -3,9 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
require Logger

View file

@ -3,11 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.User
require Pleroma.Constants
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp user_has_posted?(%User{} = u), do: u.note_count > 0

View file

@ -3,10 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
require Logger
@moduledoc "Drop and log everything received"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Logger
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(activity) do
Logger.debug("REJECTING #{inspect(activity)}")

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.Object.Updater
@ -10,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
@moduledoc "Reject or force-unlisted emojis with certain URLs or names"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp config_remove_url do
Pleroma.Config.get([:mrf_emoji, :remove_url], [])

View file

@ -3,11 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
alias Pleroma.Object
@moduledoc "Ensure a re: is prepended on replies to a post with a Subject"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Object
use Pleroma.Web.ActivityPub.MRF.Policy
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
def history_awareness, do: :auto

View file

@ -4,12 +4,15 @@
defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
require Logger
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(activity) do
with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]),

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@moduledoc "Remove bot posts from federated timeline"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
use Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants

View file

@ -3,13 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp get_author(url) do
with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),

View file

@ -3,13 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.Config
@ -14,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
"""
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :manual

View file

@ -3,13 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@moduledoc "Block activities with too much mentions (configurable)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
require Pleroma.Constants
@moduledoc "Block activities with too much mentions (configurable)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp delist_activity(activity, threshold) when threshold > 0 do
follower_collection = User.get_cached_by_ap_id(activity["actor"]).follower_address

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
@moduledoc "Force a quote line into the message content."
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp build_inline_quote(template, url) do
quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>")

View file

@ -3,13 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace activities with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.Web.ActivityPub.MRF.Utils
@moduledoc "Reject or Word-Replace activities with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
@moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.HTTP
alias Pleroma.Web.MediaProxy

View file

@ -4,9 +4,10 @@
defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
@moduledoc "Block activities which mention a user"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(%{"type" => "Create"} = activity) do
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@moduledoc "Filter local activities which have no content"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Web.Endpoint
@impl true

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
@moduledoc "Does nothing (lets the messages go through unmodified)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(activity) do
{:ok, activity}

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto

View file

@ -4,9 +4,11 @@
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@moduledoc "Scrub configured hypertext markup"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.HTML
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto

View file

@ -38,6 +38,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
- `unlist`: Unlist all detected NSFW content? Default: `false`
- `reject`: Reject all detected NSFW content (takes precedence)? Default: `false`
"""
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.HTTP
@ -46,7 +48,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
require Logger
require Pleroma.Constants
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@policy :mrf_nsfw_api
def build_request_url(url) do

View file

@ -3,13 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.User
require Pleroma.Constants
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp check_date(%{"object" => %{"published" => published}} = activity) do
with %DateTime{} = now <- DateTime.utc_now(),

View file

@ -15,4 +15,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
}
@callback history_awareness() :: :auto | :manual
@optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1
defmacro __using__(_) do
quote do
# Otherwise Pleroma.Docs.Generator.list_behaviour_implementations fails
# since the Elixir compiler doesn't automatically load the module since v1.19.
@compile {:autoload, true}
end
end
end

View file

@ -6,11 +6,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do
@moduledoc """
QuietReply alters the scope of activities from local users when replying by enforcing them to be "Unlisted" or "Quiet Public". This delivers the activity to all the expected recipients and instances, but it will not be published in the Federated / The Whole Known Network timelines. It will still be published to the Home timelines of the user's followers and visible to anyone who opens the thread.
"""
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
@moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
require Pleroma.Constants

View file

@ -4,11 +4,12 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@moduledoc "Rejects non-public (followers-only, direct) activities"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants

View file

@ -2,6 +2,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy do
@moduledoc "Drop remote reports if they don't contain enough information."
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
@impl true

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@moduledoc "Filter activities depending on their origin instance"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.FollowingRelationship
alias Pleroma.User

View file

@ -3,12 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
@moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Logger
alias Pleroma.Config
@moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])

View file

@ -3,12 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
defp lookup_subchain(actor) do
with matches <- Config.get([:mrf_subchain, :match_actor]),

View file

@ -3,8 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@moduledoc """
Apply policies based on user tags
@ -18,6 +16,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
- `mrf_tag:disable-remote-subscription`: Reject non-local follow requests
- `mrf_tag:disable-any-subscription`: Reject any follow requests
"""
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
use Pleroma.Web.ActivityPub.MRF.Policy
require Pleroma.Constants

View file

@ -3,11 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
alias Pleroma.Config
@moduledoc "Accept-list of users from specified instances"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.Config
use Pleroma.Web.ActivityPub.MRF.Policy
defp filter_by_list(activity, []), do: {:ok, activity}
defp filter_by_list(%{"actor" => actor} = activity, allow_list) do

View file

@ -4,9 +4,10 @@
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@moduledoc "Filter activities which belong to certain activity vocabularies"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
use Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(%{"type" => "Undo", "object" => object} = activity) do
with {:ok, _} <- filter(object) do

View file

@ -68,7 +68,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
capabilities =
if is_boolean(user.accepts_chat_messages) do
%{
"acceptsChatMessages" => user.accepts_chat_messages
"acceptsChatMessages" => Pleroma.Chat.enabled?() and user.accepts_chat_messages
}
else
%{}

View file

@ -71,7 +71,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end
def descriptions(conn, _params) do
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
descriptions =
Pleroma.Docs.JSON.compiled_descriptions()
|> Enum.filter(&allowed_config?(&1, :whitelist))
|> Enum.reject(&allowed_config?(&1, :blacklist))
json(conn, translate_descriptions(descriptions))
end
@ -132,7 +135,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
with :ok <- configurable_from_database() do
results =
configs
|> Enum.filter(&whitelisted_config?/1)
|> Enum.filter(&allowed_config?(&1, :whitelist))
|> Enum.reject(&allowed_config?(&1, :blacklist))
|> Enum.map(fn
%{group: group, key: key, delete: true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
@ -174,27 +178,34 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end
end
defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false
defp allowed_config?(":pleroma", ":database_config_whitelist", _), do: true
defp allowed_config?(":pleroma", ":database_config_blacklist", _), do: true
defp whitelisted_config?(group, key) do
if whitelisted_configs = Config.get(:database_config_whitelist) do
Enum.any?(whitelisted_configs, fn
{whitelisted_group} ->
group == inspect(whitelisted_group)
defp allowed_config?(group, key, mode) do
configs =
case mode do
:whitelist -> Config.get(:database_config_whitelist)
:blacklist -> Config.get(:database_config_blacklist)
end
{whitelisted_group, whitelisted_key} ->
group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
if configs do
Enum.any?(configs, fn
{allowed_group} ->
group == inspect(allowed_group)
{allowed_group, allowed_key} ->
group == inspect(allowed_group) && key == inspect(allowed_key)
end)
else
true
end
end
defp whitelisted_config?(%{group: group, key: key}) do
whitelisted_config?(group, key)
defp allowed_config?(%{group: group, key: key}, mode) do
allowed_config?(group, key, mode)
end
defp whitelisted_config?(%{group: group} = config) do
whitelisted_config?(group, config[:key])
defp allowed_config?(%{group: group} = config, mode) do
allowed_config?(group, config[:key], mode)
end
end

View file

@ -31,7 +31,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The updated chat",
"application/json",
Chat
)
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -56,7 +57,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The read ChatMessage",
"application/json",
ChatMessage
)
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -87,7 +89,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The existing chat",
"application/json",
Chat
)
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -118,7 +121,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The created or existing chat",
"application/json",
Chat
)
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -146,7 +150,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
200 => Operation.response("The chats of the user", "application/json", chats_response()),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -172,7 +177,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
| pagination_params()
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
200 => Operation.response("The chats of the user", "application/json", chats_response()),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -224,6 +230,7 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
ChatMessage
),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError),
422 => Operation.response("MRF Rejection", "application/json", ApiError)
},
security: [
@ -249,7 +256,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"The deleted ChatMessage",
"application/json",
ChatMessage
)
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -268,7 +276,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
],
responses: %{
200 => Operation.response("The existing chat", "application/json", Chat)
200 => Operation.response("The existing chat", "application/json", Chat),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
@ -287,7 +296,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
],
responses: %{
200 => Operation.response("The existing chat", "application/json", Chat)
200 => Operation.response("The existing chat", "application/json", Chat),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{

View file

@ -139,6 +139,8 @@ defmodule Pleroma.Web.Endpoint do
from: {:pleroma, "priv/static/adminfe/"}
)
plug(Pleroma.Web.Plugs.StaticNotFoundPlug)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do

View file

@ -89,7 +89,8 @@ defmodule Pleroma.Web.Federator do
# Job Worker Callbacks
@spec perform(atom(), any()) :: {:ok, any()} | {:error, any()}
@impl true
@spec perform(atom(), any()) :: :ok | {:ok, any()} | {:error, any()}
def perform(:publish_one, params) do
Publisher.prepare_one(params)
|> Publisher.publish_one()

View file

@ -3,5 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publishing do
@callback perform(atom(), any()) :: :ok | {:ok, any()} | {:error, any()}
@callback publish(map()) :: any()
end

View file

@ -38,6 +38,20 @@ defmodule Pleroma.Web.Feed.FeedView do
end
end
def most_recent_update([], :rss), do: nil
def most_recent_update(activities, :rss) do
activities
|> Enum.map(fn activity ->
case activity.object do
%Object{updated_at: updated_at} -> updated_at
_ -> activity.updated_at
end
end)
|> Enum.max_by(&NaiveDateTime.to_erl/1)
|> to_rfc2822()
end
def most_recent_update(activities, user, :atom) do
(List.first(activities) || user).updated_at
|> to_rfc3339()
@ -97,6 +111,67 @@ defmodule Pleroma.Web.Feed.FeedView do
end
end
def rss_content_encoded(data) do
data
|> feed_content_encoded()
|> String.replace("]]>", "]]&gt;")
end
def atom_content_encoded(data), do: feed_content_encoded(data)
defp feed_content_encoded(data) do
base_content =
case activity_content(data) do
"" ->
(data["summary"] || data["type"] || "")
|> escape()
content ->
content
end
attachments_html =
if sensitive?(data) do
""
else
(data["attachment"] || [])
|> Enum.map(&rss_attachment_preview/1)
|> Enum.reject(&(&1 == ""))
|> Enum.join("<br/><br/>")
end
[base_content, attachments_html]
|> Enum.reject(&(&1 == ""))
|> Enum.join("<br/><br/>")
end
defp rss_attachment_preview(attachment) do
href = attachment_href(attachment)
if is_binary(href) and href != "" do
media_type = attachment_type(attachment) || "application/octet-stream"
escaped_href = escape(href)
name = attachment["name"] || "Attachment"
escaped_name = escape(name)
cond do
String.starts_with?(media_type, "image/") ->
~s(<p><img src="#{escaped_href}" alt="#{escaped_name}" loading="lazy"/></p>)
String.starts_with?(media_type, "video/") ->
~s(<p><video controls preload="metadata" src="#{escaped_href}"></video></p><p><a href="#{escaped_href}">#{escaped_name}</a></p>)
String.starts_with?(media_type, "audio/") ->
~s(<p><audio controls preload="metadata" src="#{escaped_href}"></audio></p><p><a href="#{escaped_href}">#{escaped_name}</a></p>)
true ->
~s(<p><a href="#{escaped_href}">#{escaped_name}</a></p>)
end
else
""
end
end
def activity_content(%{"content" => content}) do
content
|> String.replace(~r/[\n\r]/, "")
@ -106,16 +181,104 @@ defmodule Pleroma.Web.Feed.FeedView do
def activity_context(activity), do: escape(activity.data["context"])
def sensitive?(data), do: data["sensitive"] in [true, "true"]
def feed_self_url(conn) do
conn
|> Phoenix.Controller.current_url()
|> escape()
end
def activity_link(activity, data) do
cond do
activity.local -> data["id"] || data["url"]
true -> data["external_url"] || data["id"] || data["url"]
end
end
def attachment_href(attachment) do
attachment["url"]
|> hd()
attachment
|> attachment_url_data()
|> Map.get("href")
end
def attachment_type(attachment) do
attachment["url"]
|> hd()
|> Map.get("mediaType")
attachment
|> attachment_url_data()
|> Map.get("mediaType") || attachment["mediaType"]
end
def attachment_size(attachment) do
attachment
|> attachment_url_data()
|> Map.get("size", 0)
end
def attachment_size_positive(attachment) do
case attachment_size(attachment) do
size when is_integer(size) and size > 0 ->
size
size when is_binary(size) ->
case Integer.parse(size) do
{parsed, ""} when parsed > 0 -> parsed
_ -> nil
end
_ ->
nil
end
end
def attachment_medium(attachment) do
case attachment_type(attachment) do
"image/" <> _rest -> "image"
"video/" <> _rest -> "video"
"audio/" <> _rest -> "audio"
_ -> "document"
end
end
def rss_enclosure_attachment(attachments) do
Enum.find(attachments || [], &(not is_nil(attachment_size_positive(&1))))
end
def attachment_description(attachment) do
attachment["name"] || ""
end
def media_content_xml(_attachment, sensitive) when sensitive in [true, "true"], do: ""
def media_content_xml(attachment, _sensitive) do
href = escape(attachment_href(attachment) || "")
type = escape(attachment_type(attachment) || "application/octet-stream")
medium = escape(attachment_medium(attachment))
file_size_attr =
case attachment_size_positive(attachment) do
size when is_integer(size) -> ~s( fileSize="#{size}")
_ -> ""
end
case attachment_description(attachment) do
"" ->
~s(<media:content url="#{href}" type="#{type}"#{file_size_attr} medium="#{medium}"/>\n)
description ->
"""
<media:content url="#{href}" type="#{type}"#{file_size_attr} medium="#{medium}">
<media:description type="plain">#{escape(description)}</media:description>
</media:content>
"""
end
end
defp attachment_url_data(attachment) do
case attachment["url"] do
[first | _] when is_map(first) -> first
%{} = map -> map
_ -> %{}
end
end
def get_href(id) do

View file

@ -716,7 +716,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
defp put_application(
params,
%{assigns: %{token: %Token{} = token, user: %User{} = user}} = _conn
) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
Map.put(params, :generator, %{type: "Application", name: client_name, url: website})

View file

@ -385,6 +385,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|> cast_params()
|> Map.update(:include_types, [], fn include_types -> include_types end)
if "pleroma:chat_mention" in options.include_types and not Pleroma.Chat.enabled?() do
options
|> Map.update(:include_types, [], fn include_types ->
List.delete(include_types, "pleroma:chat_mention")
end)
else
options
end
if ("pleroma:report" not in options.include_types and
User.privileged?(user, :reports_manage_reports)) or
User.privileged?(user, :reports_manage_reports) do

View file

@ -287,6 +287,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
user.last_status_at &&
user.last_status_at |> NaiveDateTime.to_date() |> Date.to_iso8601()
accepts_chat_messages = Pleroma.Chat.enabled?() and user.accepts_chat_messages
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@ -336,7 +338,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
relationship: relationship,
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
accepts_chat_messages: accepts_chat_messages,
favicon: favicon,
avatar_description: avatar_description,
header_description: header_description
@ -391,6 +393,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> Kernel.put_in([:source, :pleroma, :show_role], user.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text)
|> Kernel.put_in([:source, :pleroma, :show_birthday], user.show_birthday)
|> Kernel.put_in([:source, :pleroma, :accepts_chat_messages], user.accepts_chat_messages)
end
defp maybe_put_settings(data, _, _, _), do: data

View file

@ -202,8 +202,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end,
"pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages",
"pleroma:pin_chats",
if Pleroma.Chat.enabled?() do
"pleroma_chat_messages"
end,
if Pleroma.Chat.enabled?() do
"pleroma:pin_chats"
end,
if Config.get([:instance, :show_reactions]) do
"exposable_reactions"
end,

View file

@ -41,6 +41,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
)
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:ensure_chats_enabled)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
@ -244,4 +245,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
_ -> nil
end
end
defp ensure_chats_enabled(conn, _opts) do
if Chat.enabled?() do
conn
else
conn
|> put_status(:not_found)
|> put_view(Pleroma.Web.ErrorView)
|> render("404.json")
|> halt()
end
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do
@moduledoc "Ensures Host header matches instance"
@behaviour Plug
alias Pleroma.Web.Endpoint

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1]
import Plug.Conn, only: [put_resp_header: 3]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@ -51,37 +51,10 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
|> Map.put(:from, from)
|> Map.put(:content_types, false)
conn =
conn
|> set_content_type(conn.request_path)
|> Plug.Static.call(opts)
conn = set_content_type(conn, conn.request_path)
if conn.halted do
conn
else
path = String.trim_leading(conn.request_path, "/")
if not File.exists?(file_path(path)) do
conn
|> put_status(:not_found)
|> send_404()
|> halt()
else
conn
end
end
end
defp send_404(conn) do
if String.ends_with?(String.downcase(conn.request_path), ".json") do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(%{error: "not found"}))
else
conn
|> put_resp_header("content-type", "text/plain")
|> send_resp(404, "Not found")
end
# Call Plug.Static with our sanitized content-type
Plug.Static.call(conn, opts)
end
defp set_content_type(conn, "/emoji/" <> filepath) do

View file

@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
where: t.token == ^token
)
with %Token{user_id: user_id} = token_record <- Repo.one(token_query) |> Repo.preload(:user),
with %Token{user_id: user_id} = token_record <- Repo.one(token_query),
false <- is_nil(user_id),
%User{} = user <- User.get_cached_by_id(user_id) do
{:ok, user, token_record}

View file

@ -0,0 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.StaticNotFoundPlug do
@moduledoc """
A small plug that enforces returning 404 on static files when they don't exist.
"""
@behaviour Plug
require Pleroma.Constants
import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1]
def init(opts), do: opts
for only <- Pleroma.Constants.static_only_files() do
def call(%{request_path: "/" <> unquote(only) <> _} = conn, _opts) do
send_404(conn)
end
end
def call(conn, _) do
conn
end
defp send_404(conn) do
if String.ends_with?(String.downcase(conn.request_path), ".json") do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(%{error: "not found"}))
else
conn
|> put_resp_header("content-type", "text/plain")
|> send_resp(404, "Not found")
end
|> halt()
end
end

View file

@ -195,6 +195,9 @@ defmodule Pleroma.Web.Streamer do
recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
following_ap_ids =
if user.domain_blocks == [], do: [], else: User.get_cached_user_friends_ap_ids(user)
with parent <- Object.normalize(item, fetch: false) || item,
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
@ -205,8 +208,12 @@ defmodule Pleroma.Web.Streamer do
true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
false <-
item.actor not in following_ap_ids and
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <-
parent.data["actor"] not in following_ap_ids and
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
false <- CommonAPI.thread_muted?(parent, user) do
false
@ -289,15 +296,17 @@ defmodule Pleroma.Web.Streamer do
defp do_stream(topic, {user, %MessageReference{} = cm_ref})
when topic in ["user", "user:pleroma_chat"] do
topic = "#{topic}:#{user.id}"
if Pleroma.Chat.enabled?() do
topic = "#{topic}:#{user.id}"
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic)
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
end)
end)
end)
end
end
defp do_stream("user", %Activity{} = item) do

View file

@ -1,9 +1,9 @@
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id>
<id><%= escape(@data["id"] || "") %></id>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_description(@data) %></content>
<content type="html"><%= escape(atom_content_encoded(@data)) %></content>
<published><%= to_rfc3339(@data["published"]) %></published>
<updated><%= to_rfc3339(@data["published"]) %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
@ -15,35 +15,26 @@
<summary><%= escape(@data["summary"]) %></summary>
<% end %>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
<link type="text/html" href='<%= @data["id"] %>' rel="alternate"/>
<% else %>
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
<link type="text/html" href='<%= escape(activity_link(@activity, @data) || "") %>' rel="alternate"/>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<category term="<%= tag %>"></category>
<% end %>
<%= for attachment <- @data["attachment"] || [] do %>
<link rel="enclosure" href="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
<%= unless sensitive?(@data) do %>
<%= for attachment <- @data["attachment"] || [] do %>
<%= if size = attachment_size_positive(attachment) do %>
<link rel="enclosure" href="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>" length="<%= size %>"/>
<% else %>
<link rel="enclosure" href="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>"/>
<% end %>
<% end %>
<% end %>
<%= if @data["inReplyTo"] do %>
<thr:in-reply-to ref='<%= @data["inReplyTo"] %>' href='<%= get_href(@data["inReplyTo"]) %>'/>
<% end %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="<%= id %>"/>
<% end %>
<% end %>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji" href="<%= file %>"/>
<% end %>

View file

@ -1,45 +1,34 @@
<item>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<guid><%= @data["id"] %></guid>
<guid isPermaLink="true"><%= escape(@data["id"] || "") %></guid>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_description(@data) %></description>
<content:encoded><![CDATA[<%= rss_content_encoded(@data) %>]]></content:encoded>
<pubDate><%= to_rfc2822(@data["published"]) %></pubDate>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<%= if @activity.local do %>
<link><%= @data["id"] %></link>
<% else %>
<link><%= @data["external_url"] %></link>
<% end %>
<link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
<link><%= escape(activity_link(@activity, @data) || "") %></link>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<category term="<%= tag %>"></category>
<%= if tag && tag != "" do %>
<category><%= tag %></category>
<% end %>
<% end %>
<%= unless sensitive?(@data) do %>
<%= with attachment when not is_nil(attachment) <- rss_enclosure_attachment(@data["attachment"]) do %>
<enclosure url="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>" length="<%= attachment_size_positive(attachment) %>" />
<% end %>
<% end %>
<%= for attachment <- @data["attachment"] || [] do %>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>" />
<%= media_content_xml(attachment, sensitive?(@data)) %>
<% end %>
<%= if @data["inReplyTo"] do %>
<thr:in-reply-to ref='<%= @data["inReplyTo"] %>' href='<%= get_href(@data["inReplyTo"]) %>'/>
<% end %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection">http://activityschema.org/collection/public</link>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person"><%= id %></link>
<% end %>
<% end %>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji"><%= file %></link>
<% end %>
</item>

View file

@ -4,9 +4,9 @@
<%= render Phoenix.Controller.view_module(@conn), "_tag_author.atom", assigns %>
<id><%= @data["id"] %></id>
<id><%= escape(@data["id"] || "") %></id>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_description(@data) %></content>
<content type="html"><%= escape(atom_content_encoded(@data)) %></content>
<published><%= to_rfc3339(@data["published"]) %></published>
<updated><%= to_rfc3339(@data["published"]) %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
@ -15,34 +15,25 @@
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] != "" do %>
<summary><%= @data["summary"] %></summary>
<summary><%= escape(@data["summary"]) %></summary>
<% end %>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
<link type="text/html" href='<%= @data["id"] %>' rel="alternate"/>
<% else %>
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned"
ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"
href="http://activityschema.org/collection/public"/>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>
<link rel="mentioned"
ostatus:object-type="http://activitystrea.ms/schema/1.0/person"
href="<%= id %>" />
<% end %>
<% end %>
<% end %>
<link type="text/html" href='<%= escape(activity_link(@activity, @data) || "") %>' rel="alternate"/>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<category term="<%= tag %>"></category>
<% end %>
<%= unless sensitive?(@data) do %>
<%= for attachment <- @data["attachment"] || [] do %>
<%= if size = attachment_size_positive(attachment) do %>
<link rel="enclosure" href="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>" length="<%= size %>"/>
<% else %>
<link rel="enclosure" href="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>"/>
<% end %>
<% end %>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji" href="<%= file %>"/>
<% end %>

View file

@ -1,14 +1,24 @@
<item>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<guid isPermalink="true"><%= activity_context(@activity) %></guid>
<link><%= activity_context(@activity) %></link>
<guid isPermaLink="true"><%= escape(activity_link(@activity, @data) || "") %></guid>
<link><%= escape(activity_link(@activity, @data) || "") %></link>
<pubDate><%= to_rfc2822(@data["published"]) %></pubDate>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_description(@data) %></description>
<%= for attachment <- @data["attachment"] || [] do %>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
<content:encoded><![CDATA[<%= rss_content_encoded(@data) %>]]></content:encoded>
<%= unless sensitive?(@data) do %>
<%= with attachment when not is_nil(attachment) <- rss_enclosure_attachment(@data["attachment"]) do %>
<enclosure url="<%= escape(attachment_href(attachment) || "") %>" type="<%= escape(attachment_type(attachment) || "application/octet-stream") %>" length="<%= attachment_size_positive(attachment) %>"/>
<% end %>
<% end %>
<%= for attachment <- @data["attachment"] || [] do %>
<%= media_content_xml(attachment, sensitive?(@data)) %>
<% end %>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<%= if tag && tag != "" do %>
<category><%= tag %></category>
<% end %>
<% end %>
</item>

View file

@ -9,10 +9,12 @@
<id><%= Routes.tag_feed_url(@conn, :feed, @tag) <> ".atom" %></id>
<title>#<%= @tag %></title>
<subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<%= with subtitle when subtitle not in [nil, ""] <- Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) do %>
<subtitle><%= subtitle %></subtitle>
<% end %>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
<link rel="self" href="<%= "#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom" %>" type="application/atom+xml"/>
<updated><%= most_recent_update(@activities) || to_rfc3339(NaiveDateTime.utc_now()) %></updated>
<link rel="self" href="<%= feed_self_url(@conn) %>" type="application/atom+xml"/>
<%= for activity <- @activities do %>
<%= render Phoenix.Controller.view_module(@conn), "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:webfeeds="http://webfeeds.org/rss/1.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:thr="http://purl.org/syndication/thread/1.0">
<channel>
@ -8,7 +10,12 @@
<description><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></description>
<link><%= "#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss" %></link>
<webfeeds:logo><%= feed_logo() %></webfeeds:logo>
<webfeeds:icon><%= feed_logo() %></webfeeds:icon>
<webfeeds:accentColor>2b90d9</webfeeds:accentColor>
<%= with last_build_date when not is_nil(last_build_date) <- most_recent_update(@activities, :rss) do %>
<lastBuildDate><%= last_build_date %></lastBuildDate>
<% end %>
<generator><%= Pleroma.Application.named_version() %></generator>
<%= for activity <- @activities do %>
<%= render Phoenix.Controller.view_module(@conn), "_tag_activity.xml", Map.merge(assigns, prepare_activity(activity)) %>
<% end %>

View file

@ -8,10 +8,12 @@
<id><%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>
<title><%= @user.nickname <> "'s timeline" %></title>
<subtitle><%= escape(@user.bio) %></subtitle>
<%= if @user.bio && @user.bio != "" do %>
<subtitle><%= escape(@user.bio) %></subtitle>
<% end %>
<updated><%= most_recent_update(@activities, @user, :atom) %></updated>
<logo><%= logo(@user) %></logo>
<link rel="self" href="<%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom" %>" type="application/atom+xml"/>
<link rel="self" href="<%= feed_self_url(@conn) %>" type="application/atom+xml"/>
<%= render Phoenix.Controller.view_module(@conn), "_author.atom", assigns %>

View file

@ -1,26 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:webfeeds="http://webfeeds.org/rss/1.0"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:poco="http://portablecontacts.net/spec/1.0">
<channel>
<title><%= @user.nickname <> "'s timeline" %></title>
<link><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss" %></link>
<atom:link href="<%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %>"
<atom:link href="<%= feed_self_url(@conn) %>"
rel="self" type="application/rss+xml" />
<link><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss" %></link>
<description><%= escape(@user.bio) %></description>
<image>
<url><%= logo(@user) %></url>
<title><%= @user.nickname <> "'s timeline" %></title>
<link><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss" %></link>
</image>
<lastBuildDate><%= most_recent_update(@activities, @user, :rss) %></lastBuildDate>
<webfeeds:icon><%= logo(@user) %></webfeeds:icon>
<generator><%= Pleroma.Application.named_version() %></generator>
<%= render Phoenix.Controller.view_module(@conn), "_author.rss", assigns %>
<%= if last_activity(@activities) do %>
<link rel="next"><%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}" %></link>
<atom:link rel="next" href="<%= "#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}" %>" />
<% end %>
<%= for activity <- @activities do %>

View file

@ -4,25 +4,39 @@
defmodule Pleroma.Workers.PublisherWorker do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Instances
alias Pleroma.Web.Federator
use Oban.Worker, queue: :federator_outgoing, max_attempts: 13
@publish_one_param_keys %{
"activity_id" => :activity_id,
"cc" => :cc,
"inbox" => :inbox,
"unreachable_since" => :unreachable_since
}
@impl true
def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do
activity = Activity.get_by_id(activity_id)
Federator.perform(:publish, activity)
federator().perform(:publish, activity)
end
def perform(%Job{args: %{"op" => "publish_one", "params" => params}} = job) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
params =
Enum.reduce(params, %{}, fn {key, value}, acc ->
case @publish_one_param_keys do
%{^key => atom_key} -> Map.put(acc, atom_key, value)
_ -> acc
end
end)
# Cancel / skip the job if this server believed to be unreachable now
if not Instances.reachable?(params.inbox) do
{:cancel, :unreachable}
else
case Federator.perform(:publish_one, params) do
case federator().perform(:publish_one, params) do
{:ok, _} ->
:ok
@ -44,6 +58,8 @@ defmodule Pleroma.Workers.PublisherWorker do
@impl true
def timeout(_job), do: :timer.seconds(10)
defp federator, do: Config.get([:pipeline, :federator], Federator)
@base_backoff 15
@pow 5
@impl true