Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
1 MB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/changelog.d/captcha-package.fix b/changelog.d/captcha-package.fix
new file mode 100644
index 000000000..98c2f6525
--- /dev/null
+++ b/changelog.d/captcha-package.fix
@@ -0,0 +1 @@
+Switch native captcha to the published pleroma_captcha Hex package.
diff --git a/changelog.d/host-header-verification.security b/changelog.d/host-header-verification.security
new file mode 100644
index 000000000..11a0784b2
--- /dev/null
+++ b/changelog.d/host-header-verification.security
@@ -0,0 +1 @@
+Ensure Host header is present and matches instance URI
diff --git a/changelog.d/http-signatures-0.1.3.fix b/changelog.d/http-signatures-0.1.3.fix
new file mode 100644
index 000000000..3be1b9e9f
--- /dev/null
+++ b/changelog.d/http-signatures-0.1.3.fix
@@ -0,0 +1 @@
+Fix compatibility with timestamped HTTP Signatures used by GoToSocial
diff --git a/changelog.d/iceshrimpnet-reports-fix.fix b/changelog.d/iceshrimpnet-reports-fix.fix
new file mode 100644
index 000000000..7487ae47e
--- /dev/null
+++ b/changelog.d/iceshrimpnet-reports-fix.fix
@@ -0,0 +1 @@
+Handle reports with just actor ap id as the object
diff --git a/changelog.d/majic-1.2.0.change b/changelog.d/majic-1.2.0.change
new file mode 100644
index 000000000..bc8d5eb3c
--- /dev/null
+++ b/changelog.d/majic-1.2.0.change
@@ -0,0 +1 @@
+Update majic to 1.2.0.
diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add
new file mode 100644
index 000000000..f815c6828
--- /dev/null
+++ b/changelog.d/mfm-backend.add
@@ -0,0 +1 @@
+Add backend support for Misskey Markdown (MFM) posts
diff --git a/changelog.d/oban-plugins-lazarus-hex.change b/changelog.d/oban-plugins-lazarus-hex.change
new file mode 100644
index 000000000..9259df0f5
--- /dev/null
+++ b/changelog.d/oban-plugins-lazarus-hex.change
@@ -0,0 +1 @@
+Use the Hex package for oban_plugins_lazarus.
diff --git a/changelog.d/phoenix-sec-websocket-headers.change b/changelog.d/phoenix-sec-websocket-headers.change
new file mode 100644
index 000000000..b3cb4ad3f
--- /dev/null
+++ b/changelog.d/phoenix-sec-websocket-headers.change
@@ -0,0 +1 @@
+Switch patched Phoenix 1.7.14 back to upstream with Phoenix 1.8.0+
diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix
new file mode 100644
index 000000000..de93f86dd
--- /dev/null
+++ b/changelog.d/pleroma-fe-link.fix
@@ -0,0 +1 @@
+Updated Pleroma-FE build URL after Forgejo migration
diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix
new file mode 100644
index 000000000..7eae41f13
--- /dev/null
+++ b/changelog.d/poll-voters-count-inflation.fix
@@ -0,0 +1 @@
+Fix votersCount inflation when same voter picks multiple options
diff --git a/changelog.d/reject-third-party-reports.fix b/changelog.d/reject-third-party-reports.fix
new file mode 100644
index 000000000..7d4e87b94
--- /dev/null
+++ b/changelog.d/reject-third-party-reports.fix
@@ -0,0 +1 @@
+Reject incoming reports when both the reporter and reported account are remote
diff --git a/changelog.d/remote-ip-hex.change b/changelog.d/remote-ip-hex.change
new file mode 100644
index 000000000..44f6bc1bf
--- /dev/null
+++ b/changelog.d/remote-ip-hex.change
@@ -0,0 +1 @@
+Use upstream Hex releases for the `remote_ip` dependency and expose client IP ranges for remote IP resolution.
diff --git a/changelog.d/user-search-sorting.change b/changelog.d/user-search-sorting.change
new file mode 100644
index 000000000..def27e955
--- /dev/null
+++ b/changelog.d/user-search-sorting.change
@@ -0,0 +1 @@
+Improve user search / autocompletion ordering.
diff --git a/config/config.exs b/config/config.exs
index 66728bf19..705fd4c6a 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,975 +1,977 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
import Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: :pg_advisory_lock
config :pleroma, Pleroma.Captcha,
enabled: true,
seconds_valid: 300,
method: Pleroma.Captcha.Native
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: false,
proxy_remote: false,
filename_display_max_length: 30,
default_description: nil,
base_url: nil,
allowed_mime_types: ["image", "audio", "video"]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
bucket_namespace: nil,
truncated_namespace: nil,
streaming_enabled: true
config :ex_aws, :s3,
# host: "s3.wasabisys.com", # required if not Amazon AWS
access_key_id: nil,
secret_access_key: nil,
# region: "us-east-1", # may be required for Amazon AWS
scheme: "https://"
config :pleroma, Pleroma.Uploaders.IPFS,
post_gateway_url: "http://localhost:5001",
get_gateway_url: "http://localhost:8080"
config :pleroma, :emoji,
shortcode_globs: ["/emoji/custom/**/*.png"],
pack_extensions: [".png", ".gif"],
groups: [
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"hyper",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1}
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
live_view: [signing_salt: "U5ELgdEwTD3n1+D5s0rY0AMg8/y1STxZ3Zvsl3bWh+oBcGrYdil0rXqPMRd3Glcq"],
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub_server: Pleroma.PubSub,
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]
# Configures Elixir's Logger
config :logger, backends: [:console]
config :logger, :console,
level: :debug,
format: "\n$time $metadata[$level] $message\n",
metadata: [:actor, :path, :type, :user]
config :logger, :ex_syslogger,
level: :debug,
ident: "pleroma",
format: "$metadata[$level] $message",
metadata: [:actor, :path, :type, :user]
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"],
# Can be removed when bumping MIME past 2.0.5
# see https://akkoma.dev/AkkomaGang/akkoma/issues/657
"image/apng" => ["apng"]
}
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
send_user_agent: true,
user_agent: :default,
adapter: []
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "Pleroma: An efficient and flexible fediverse server",
short_description: "",
background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg",
favicon: "/favicon.png",
limit: 5_000,
description_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
invites_enabled: false,
account_activation_required: false,
account_approval_required: false,
federating: true,
federation_incoming_replies_max_depth: 100,
allow_relay: true,
public: true,
quarantined_instances: [],
rejected_instances: [],
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
- "text/bbcode"
+ "text/bbcode",
+ "text/x.misskeymarkdown"
],
autofollowed_nicknames: [],
autofollowing_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
max_report_comment_size: 1000,
report_strip_status: true,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
registration_reason_length: 500,
external_user_synchronization: true,
extended_nickname_format: true,
cleanup_attachments: false,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 5,
length: 16
]
],
show_reactions: true,
password_reset_token_validity: 60 * 60 * 24,
profile_directory: true,
admin_privileges: [
:users_read,
:users_manage_invites,
:users_manage_activation_state,
:users_manage_tags,
:users_manage_credentials,
:users_delete,
:messages_read,
:messages_delete,
:instances_delete,
:reports_manage_reports,
:moderation_log_read,
:announcements_manage_announcements,
:emoji_manage_emoji,
:statistics_read
],
moderator_privileges: [:messages_delete, :reports_manage_reports],
max_endorsed_users: 20,
birthday_required: false,
birthday_min_age: 0,
max_media_attachments: 1_000
config :pleroma, :welcome,
direct_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
chat_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
email: [
enabled: false,
sender: nil,
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
config :pleroma, :feed,
post_title: %{
max_length: 100,
omission: "..."
}
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
allow_inline_images: true,
allow_headings: false,
allow_tables: false,
allow_fonts: false,
scrub_policy: [
Pleroma.HTML.Scrubber.Default,
Pleroma.HTML.Transform.MediaProxy
]
config :pleroma, :frontend_configurations,
pleroma_fe: %{
alwaysShowSubjectInput: true,
background: "/images/city.jpg",
collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
embeddedToS: true,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: true,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
subjectLineBehavior: "email",
theme: "pleroma-dark",
webPushNotifications: false
}
config :pleroma, :assets,
mascots: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
],
default_mascot: :pleroma_fox_tan
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.svg",
sizes: "512x512",
purpose: "any",
type: "image/svg+xml"
}
],
theme_color: "#282c37",
background_color: "#191b22"
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: true,
blockers_visible: true,
follow_handshake_timeout: 500,
note_replies_output_limit: 5,
sign_object_fetches: true,
authorized_fetch_mode: false,
client_api_enabled: false,
anonymize_reporter: false,
anonymize_reporter_local_nickname: ""
config :pleroma, :streamer,
workers: 3,
overflow_workers: 2
config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_normalize_markup, scrub_policy: Pleroma.HTML.Scrubber.Default
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
followers_only: [],
accept: [],
avatar_removal: [],
banner_removal: [],
reject_deletes: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :mrf_emoji,
remove_url: [],
remove_shortcode: [],
federated_timeline_removal_url: [],
federated_timeline_removal_shortcode: []
config :pleroma, :mrf_hashtag,
sensitive: ["nsfw"],
reject: [],
federated_timeline_removal: []
config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365
config :pleroma, :mrf_vocabulary,
accept: [],
reject: []
# threshold of 7 days
config :pleroma, :mrf_object_age,
threshold: 604_800,
actions: [:delist, :strip_followers]
config :pleroma, :mrf_nsfw_api,
url: "http://127.0.0.1:5000/",
threshold: 0.7,
mark_sensitive: true,
unlist: false,
reject: false
config :pleroma, :mrf_follow_bot, follower_nickname: nil
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
config :pleroma, :mrf_remote_report,
reject_all: false,
reject_anonymous: true,
reject_empty_message: true
config :pleroma, :mrf_force_mention,
mention_parent: true,
mention_quoted: true
config :pleroma, :mrf_antimentionspam, user_age_limit: 30_000
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"],
parsers: [
Pleroma.Web.RichMedia.Parsers.TwitterCard,
Pleroma.Web.RichMedia.Parsers.OEmbed
],
timeout: 5_000,
ttl_setters: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl,
Pleroma.Web.RichMedia.Parser.TTL.Opengraph
],
max_body: 5_000_000
config :pleroma, :media_proxy,
enabled: false,
invalidation: [
enabled: false,
provider: Pleroma.Web.MediaProxy.Invalidation.Script
],
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
max_read_duration: 30_000,
http: [
follow_redirect: true,
pool: :media
]
],
whitelist: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge,
headers: [],
options: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: nil,
url_format: nil
# Note: media preview proxy depends on media proxy to be enabled
config :pleroma, :media_preview_proxy,
enabled: false,
thumbnail_max_width: 600,
thumbnail_max_height: 600,
image_quality: 85,
min_content_length: 100 * 1024
config :pleroma, :shout,
enabled: true,
limit: 5_000
config :phoenix, :format_encoders, json: Jason, "activity+json": Jason
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "confirm"]
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, Pleroma.Web.Metadata,
providers: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard
],
unfurl_nsfw: false
config :pleroma, Pleroma.Web.Preload,
providers: [
Pleroma.Web.Preload.Providers.Instance
]
config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin",
allow_unsafe_eval: false
config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web",
"verify_credentials",
"update_credentials",
"relationships",
"search",
"confirmation_resend",
"mfa"
],
email_blacklist: []
# The Pruner :max_age must be longer than Worker :unique
# value or it cannot enforce uniqueness.
config :pleroma, Oban,
repo: Pleroma.Repo,
notifier: Oban.Notifiers.PG,
log: false,
queues: [
activity_expiration: 10,
federator_incoming: 5,
federator_outgoing: 25,
web_push: 50,
background: 20,
search_indexing: [limit: 10, paused: true],
slow: 5
],
plugins: [Oban.Plugins.Lazarus, {Oban.Plugins.Pruner, max_age: 900}],
crontab: [
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker},
{"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}
]
config :pleroma, Pleroma.Formatter,
class: false,
rel: "ugc",
new_window: false,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST", "localhost"),
port: String.to_integer(System.get_env("LDAP_PORT", "389")),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE", "dc=example,dc=com"),
uid: System.get_env("LDAP_UID", "cn"),
# defaults to CAStore's Mozilla roots
cacertfile: System.get_env("LDAP_CACERTFILE", nil),
mail: System.get_env("LDAP_MAIL", "mail")
oauth_consumer_strategies =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(&hd(String.split(&1, ":")))
ueberauth_providers =
for strategy <- oauth_consumer_strategies do
strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}"
strategy_module = String.to_atom(strategy_module_name)
{String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}}
end
config :ueberauth,
Ueberauth,
base_path: "/oauth",
providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
config :pleroma, Pleroma.Emails.UserEmail,
logo: nil,
styling: %{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false
config :pleroma, Pleroma.PromEx,
disabled: false,
manual_metrics_start_delay: :no_delay,
drop_metrics_groups: [],
grafana: [
host: System.get_env("GRAFANA_HOST", "http://localhost:3000"),
auth_token: System.get_env("GRAFANA_TOKEN"),
upload_dashboards_on_start: false,
folder_name: "BEAM",
annotate_app_lifecycle: true
],
metrics_server: [
port: 4021,
path: "/metrics",
protocol: :http,
pool_size: 5,
cowboy_opts: [],
auth_strategy: :none
],
datasource: "Prometheus"
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 300,
enabled: true
config :pleroma, :email_notifications,
digest: %{
active: false,
interval: 7,
inactivity_threshold: 7
}
config :pleroma, :oauth2,
token_expires_in: 3600 * 24 * 365 * 100,
issue_new_refresh_token: true,
clean_expired_tokens: false
config :pleroma, :database, rum_enabled: false
config :pleroma, :features, improved_hashtag_timeline: :auto
config :pleroma, :populate_hashtags_table, fault_rate_allowance: 0.01
config :pleroma, :delete_context_objects, fault_rate_allowance: 0.01
config :pleroma, :env, Mix.env()
config :http_signatures,
adapter: Pleroma.Signature
config :pleroma, :rate_limit,
authentication: {60_000, 15},
timeline: {500, 3},
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25},
oauth_app_creation: {900_000, 5},
relations_actions: {10_000, 10},
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},
status_id_action: {60_000, 3},
password_reset: {1_800_000, 5},
account_confirmation_resend: {8_640_000, 5},
ap_routes: {60_000, 15}
config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600
config :pleroma, Pleroma.Web.Plugs.RemoteIp,
enabled: true,
headers: ["x-forwarded-for"],
+ clients: [],
proxies: [],
reserved: [
"127.0.0.0/8",
"::1/128",
"fc00::/7",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
]
config :pleroma, :static_fe, enabled: false
# Example of frontend configuration
# This example will make us serve the primary frontend from the
# frontends directory within your `:pleroma, :instance, static_dir`.
# e.g., instance/static/frontends/pleroma/develop/
#
# With no frontend configuration, the bundled files from the `static` directory will
# be used.
#
# config :pleroma, :frontends,
# primary: %{"name" => "pleroma-fe", "ref" => "develop"},
# admin: %{"name" => "admin-fe", "ref" => "stable"},
# available: %{...}
config :pleroma, :frontends,
available: %{
"kenoma" => %{
"name" => "kenoma",
"git" => "https://git.pleroma.social/lambadalambda/kenoma",
"build_url" =>
"https://git.pleroma.social/lambadalambda/kenoma/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "master"
},
"pleroma-fe" => %{
"name" => "pleroma-fe",
"git" => "https://git.pleroma.social/pleroma/pleroma-fe",
"build_url" =>
- "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build",
+ "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip",
"ref" => "develop"
},
"fedi-fe" => %{
"name" => "fedi-fe",
"git" => "https://git.pleroma.social/pleroma/fedi-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build_release",
"ref" => "master",
"custom-http-headers" => [
{"service-worker-allowed", "/"}
]
},
"admin-fe" => %{
"name" => "admin-fe",
"git" => "https://git.pleroma.social/pleroma/admin-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "develop"
},
"soapbox" => %{
"name" => "soapbox",
"git" => "https://gitlab.com/soapbox-pub/soapbox",
"build_url" =>
"https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/${ref}/download?job=build-production",
"ref" => "v3.0.0-beta.1",
"build_dir" => "static"
},
"glitch-lily" => %{
"name" => "glitch-lily",
"git" => "https://lily-is.land/infra/glitch-lily",
"build_url" =>
"https://lily-is.land/infra/glitch-lily/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "servant",
"build_dir" => "public"
},
"pl-fe" => %{
"name" => "pl-fe",
"git" => "https://github.com/mkljczk/pl-fe",
"build_url" => "https://pl.mkljczk.pl/pl-fe.zip",
"ref" => "develop",
"build_dir" => "."
}
}
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
config :pleroma, :modules, runtime_dir: "instance/modules"
config :pleroma, configurable_from_database: false
config :pleroma, Pleroma.Repo,
parameters: [gin_fuzzy_search_limit: "500", jit: "off"],
prepare: :unnamed
config :pleroma, :connections_pool,
reclaim_multiplier: 0.1,
connection_acquisition_wait: 250,
connection_acquisition_retries: 5,
max_connections: 250,
max_idle_time: 30_000,
retry: 0,
connect_timeout: 5_000
config :pleroma, :pools,
federation: [
size: 75,
max_waiting: 20,
recv_timeout: 10_000
],
media: [
size: 75,
max_waiting: 20,
recv_timeout: 15_000
],
rich_media: [
size: 25,
max_waiting: 20,
recv_timeout: 15_000
],
upload: [
size: 25,
max_waiting: 20,
recv_timeout: 15_000
],
default: [
size: 50,
max_waiting: 2,
recv_timeout: 5_000
]
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 10_000
],
media: [
max_connections: 50,
timeout: 15_000
],
rich_media: [
max_connections: 50,
timeout: 15_000
],
upload: [
max_connections: 25,
timeout: 15_000
]
config :pleroma, :majic_pool, size: 2
private_instance? = :if_instance_is_private
config :pleroma, :restrict_unauthenticated,
timelines: %{local: private_instance?, federated: private_instance?},
profiles: %{local: private_instance?, remote: private_instance?},
activities: %{local: private_instance?, remote: private_instance?}
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
config :pleroma, :mrf,
policies: [
Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy,
Pleroma.Web.ActivityPub.MRF.TagPolicy,
Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
],
transparency: true,
transparency_exclusions: []
config :tzdata, :http_client, Pleroma.HTTP.Tzdata
config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :web_push_encryption, http_client: Pleroma.HTTP.WebPush
config :pleroma, :instances_favicons, enabled: false
config :floki, :html_parser, Floki.HTMLParser.FastHtml
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator
config :pleroma, Pleroma.User.Backup,
purge_after_days: 30,
limit_days: 7,
dir: nil,
process_chunk_size: 100,
timeout: :timer.minutes(30)
config :pleroma, ConcurrentLimiter, [
{Pleroma.Search, [max_running: 30, max_waiting: 50]},
{Pleroma.Webhook.Notify, [max_running: 5, max_waiting: 200]}
]
config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
config :pleroma, Pleroma.Search.Meilisearch,
url: "http://127.0.0.1:7700/",
private_key: nil,
initial_indexing_chunk_size: 100_000
config :pleroma, Pleroma.Application,
background_migrators: true,
internal_fetch: true,
load_custom_modules: true,
max_restarts: 3,
streamer_registry: true
config :pleroma, Pleroma.Uploaders.Uploader, timeout: 30_000
config :pleroma, Pleroma.Search.QdrantSearch,
qdrant_url: "http://127.0.0.1:6333/",
qdrant_api_key: "",
openai_url: "http://127.0.0.1:11345",
# The healthcheck url has to be set to nil when used with the real openai
# API, as it doesn't have a healthcheck endpoint.
openai_healthcheck_url: "http://127.0.0.1:11345/health",
openai_model: "snowflake/snowflake-arctic-embed-xs",
openai_api_key: "",
qdrant_index_configuration: %{
vectors: %{size: 384, distance: "Cosine"}
}
config :pleroma, :database_config_whitelist, [
{:pleroma},
{:cors_plug},
{:ex_aws, :s3},
{:mime},
{:prometheus, Pleroma.Web.Endpoint.MetricsExporter},
{:web_push_encryption, :vapid_details}
]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index 174380cc4..0f7345049 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1,3641 +1,3654 @@
import Config
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
installed_frontend_options = [
%{
key: "name",
label: "Name",
type: :string,
description:
"Name of the installed frontend. Valid config must include both `Name` and `Reference` values."
},
%{
key: "ref",
label: "Reference",
type: :string,
description:
"Reference of the installed frontend to be used. Valid config must include both `Name` and `Reference` values."
}
]
frontend_options = [
%{
key: "name",
label: "Name",
type: :string,
description: "Name of the frontend."
},
%{
key: "ref",
label: "Reference",
type: :string,
description: "Reference of the frontend to be used."
},
%{
key: "git",
label: "Git Repository URL",
type: :string,
description: "URL of the git repository of the frontend"
},
%{
key: "build_url",
label: "Build URL",
type: :string,
description:
"Either an url to a zip file containing the frontend or a template to build it by inserting the `ref`. The string `${ref}` will be replaced by the configured `ref`.",
example: "https://some.url/builds/${ref}.zip"
},
%{
key: "build_dir",
label: "Build directory",
type: :string,
description: "The directory inside the zip file "
},
%{
key: "custom-http-headers",
label: "Custom HTTP headers",
type: {:list, :string},
description: "The custom HTTP headers for the frontend"
}
]
config :pleroma, :config_description, [
%{
group: :pleroma,
key: Pleroma.Upload,
type: :group,
description: "Upload general settings",
children: [
%{
key: :uploader,
type: :module,
description: "Module which will be used for uploads",
suggestions: {:list_behaviour_implementations, Pleroma.Uploaders.Uploader}
},
%{
key: :filters,
type: {:list, :module},
description:
"List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter}
},
%{
key: :link_name,
type: :boolean,
description:
"If enabled, a name parameter will be added to the URL of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`."
},
%{
key: :base_url,
label: "Base URL",
type: :string,
description:
"Base URL for the uploads. Required if you use a CDN or host attachments under a different domain.",
suggestions: [
"https://cdn-host.com"
]
},
%{
key: :proxy_remote,
type: :boolean,
description: """
Proxy requests to the remote uploader.\n
Useful if media upload endpoint is not internet accessible.
"""
},
%{
key: :filename_display_max_length,
type: :integer,
description: "Set max length of a filename to display. 0 = no limit. Default: 30"
},
%{
key: :allowed_mime_types,
label: "Allowed MIME types",
type: {:list, :string},
description:
"List of MIME (main) types uploads are allowed to identify themselves with. Other types may still be uploaded, but will identify as a generic binary to clients. WARNING: Loosening this over the defaults can lead to security issues. Removing types is safe, but only add to the list if you are sure you know what you are doing.",
suggestions: [
"image",
"audio",
"video",
"font"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.Local,
type: :group,
description: "Local uploader-related settings",
children: [
%{
key: :uploads,
type: :string,
description: "Path where user's uploads will be saved",
suggestions: [
"uploads"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.IPFS,
type: :group,
description: "IPFS uploader-related settings",
children: [
%{
key: :get_gateway_url,
type: :string,
description: "GET Gateway URL",
suggestions: [
"https://ipfs.mydomain.com/{CID}",
"https://{CID}.ipfs.mydomain.com/"
]
},
%{
key: :post_gateway_url,
type: :string,
description: "POST Gateway URL",
suggestions: [
"http://localhost:5001/"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.S3,
type: :group,
description: "S3 uploader-related settings",
children: [
%{
key: :bucket,
type: :string,
description: "S3 bucket",
suggestions: [
"bucket"
]
},
%{
key: :bucket_namespace,
type: :string,
description: "S3 bucket namespace",
suggestions: ["pleroma"]
},
%{
key: :truncated_namespace,
type: :string,
description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
" For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in Upload base_url."
},
%{
key: :streaming_enabled,
type: :boolean,
description:
"Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.Mogrify,
type: :group,
description: "Uploads mogrify filter settings",
children: [
%{
key: :args,
type: [:string, {:list, :string}, {:list, :tuple}],
description:
"List of actions for the mogrify command. It's possible to add self-written settings as string. " <>
"For example `auto-orient, strip, {\"resize\", \"3840x1080>\"}` value will be parsed into valid list of the settings.",
suggestions: [
"strip",
"auto-orient",
{"implode", "1"}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.AnonymizeFilename,
type: :group,
description: "Filter replaces the filename of the upload",
children: [
%{
key: :text,
type: :string,
description:
"Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original" <>
" filename extension by using {extension}, for example custom-file-name.{extension}.",
suggestions: [
"custom-file-name.{extension}"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.Mailer,
type: :group,
description: "Mailer-related settings",
children: [
%{
key: :enabled,
label: "Mailer Enabled",
type: :boolean
},
%{
key: :adapter,
type: :module,
description:
"One of the mail adapters listed in [Swoosh documentation](https://hexdocs.pm/swoosh/Swoosh.html#module-adapters)",
suggestions: [
Swoosh.Adapters.AmazonSES,
Swoosh.Adapters.Dyn,
Swoosh.Adapters.Gmail,
Swoosh.Adapters.Mailgun,
Swoosh.Adapters.Mailjet,
Swoosh.Adapters.Mandrill,
Swoosh.Adapters.Postmark,
Swoosh.Adapters.SMTP,
Swoosh.Adapters.Sendgrid,
Swoosh.Adapters.Sendmail,
Swoosh.Adapters.SocketLabs,
Swoosh.Adapters.SparkPost
]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :relay,
type: :string,
description: "Hostname or IP address",
suggestions: ["smtp.example.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :port,
type: :integer,
description: "SMTP port",
suggestions: ["1025"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :username,
type: :string,
description: "SMTP AUTH username",
suggestions: ["user@example.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :password,
type: :string,
description: "SMTP AUTH password",
suggestions: ["password"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :ssl,
label: "Use SSL",
type: :boolean,
description: "Use Implicit SSL/TLS. e.g. port 465"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :tls,
label: "STARTTLS Mode",
type: {:dropdown, :atom},
description: "Explicit TLS (STARTTLS) enforcement mode",
suggestions: [:if_available, :always, :never]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :auth,
label: "AUTH Mode",
type: {:dropdown, :atom},
description: "SMTP AUTH enforcement mode",
suggestions: [:if_available, :always, :never]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :retries,
type: :integer,
description: "SMTP temporary (4xx) error retries",
suggestions: [1]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendgrid},
key: :api_key,
label: "SendGrid API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_path,
type: :string,
suggestions: ["/usr/bin/sendmail"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_args,
type: :string,
suggestions: ["-N delay,failure,success"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :qmail,
label: "Qmail compat mode",
type: :boolean
},
%{
group: {:subgroup, Swoosh.Adapters.Mandrill},
key: :api_key,
label: "Mandrill API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :api_key,
label: "Mailgun API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :domain,
type: :string,
suggestions: ["YOUR_DOMAIN_NAME"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :api_key,
label: "MailJet Public API Key",
type: :string,
suggestions: ["MJ_APIKEY_PUBLIC"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :secret,
label: "MailJet Private API Key",
type: :string,
suggestions: ["MJ_APIKEY_PRIVATE"]
},
%{
group: {:subgroup, Swoosh.Adapters.Postmark},
key: :api_key,
label: "Postmark API Key",
type: :string,
suggestions: ["X-Postmark-Server-Token"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :api_key,
label: "SparkPost API key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :endpoint,
type: :string,
suggestions: ["https://api.sparkpost.com/api/v1"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :access_key,
label: "AWS Access Key",
type: :string,
suggestions: ["AWS_ACCESS_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :secret,
label: "AWS Secret Key",
type: :string,
suggestions: ["AWS_SECRET_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :region,
label: "AWS Region",
type: :string,
suggestions: ["us-east-1", "us-east-2"]
},
%{
group: {:subgroup, Swoosh.Adapters.Dyn},
key: :api_key,
label: "Dyn API Key",
type: :string,
suggestions: ["apikey"]
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :api_key,
label: "SocketLabs API Key",
type: :string,
suggestions: ["INJECTION_API_KEY"]
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :server_id,
label: "Server ID",
type: :string,
suggestions: ["SERVER_ID"]
},
%{
group: {:subgroup, Swoosh.Adapters.Gmail},
key: :access_token,
label: "GMail API Access Token",
type: :string,
suggestions: ["GMAIL_API_ACCESS_TOKEN"]
}
]
},
%{
group: :pleroma,
key: :uri_schemes,
label: "URI Schemes",
type: :group,
description: "URI schemes related settings",
children: [
%{
key: :valid_schemes,
type: {:list, :string},
description: "List of the scheme part that is considered valid to be an URL",
suggestions: [
"https",
"http",
"dat",
"dweb",
"gopher",
"hyper",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
}
]
},
%{
group: :pleroma,
key: :features,
type: :group,
description: "Customizable features",
children: [
%{
key: :improved_hashtag_timeline,
type: {:dropdown, :atom},
description:
"Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).",
suggestions: [:auto, :enabled, :disabled]
}
]
},
%{
group: :pleroma,
key: :populate_hashtags_table,
type: :group,
description: "`populate_hashtags_table` background migration settings",
children: [
%{
key: :fault_rate_allowance,
type: :float,
description:
"Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if hashtags transfer failed for all records.",
suggestions: [0.01]
},
%{
key: :sleep_interval_ms,
type: :integer,
description:
"Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)."
}
]
},
%{
group: :pleroma,
key: :delete_context_objects,
type: :group,
description: "`delete_context_objects` background migration settings",
children: [
%{
key: :fault_rate_allowance,
type: :float,
description:
"Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if context object deletion failed for all records.",
suggestions: [0.01]
},
%{
key: :sleep_interval_ms,
type: :integer,
description:
"Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)."
}
]
},
%{
group: :pleroma,
key: :instance,
type: :group,
description: "Instance-related settings",
children: [
%{
key: :name,
type: :string,
description: "Name of the instance",
suggestions: [
"Pleroma"
]
},
%{
key: :email,
label: "Admin Email Address",
type: :string,
description: "Email used to reach an Administrator/Moderator of the instance",
suggestions: [
"email@example.com"
]
},
%{
key: :notify_email,
label: "Sender Email Address",
type: :string,
description: "Envelope FROM address for mail sent via Pleroma",
suggestions: [
"notify@example.com"
]
},
%{
key: :description,
type: :string,
description:
"The instance's description. It can be seen in nodeinfo and `/api/v1/instance`",
suggestions: [
"Very cool instance"
]
},
%{
key: :short_description,
type: :string,
description:
"Shorter version of instance description. It can be seen on `/api/v1/instance`",
suggestions: [
"Cool instance"
]
},
%{
key: :status_page,
type: :string,
description: "A page where people can see the status of the server during an outage",
suggestions: [
"https://status.pleroma.example.org"
]
},
%{
key: :contact_username,
type: :string,
description: "Instance owner username",
suggestions: ["admin"]
},
%{
key: :limit,
type: :integer,
description: "Posts character limit (CW/Subject included in the counter)",
suggestions: [
5_000
]
},
%{
key: :remote_limit,
type: :integer,
description: "Hard character limit beyond which remote posts will be dropped",
suggestions: [
100_000
]
},
%{
key: :max_media_attachments,
type: :integer,
description: "Maximum number of post media attachments",
suggestions: [
1_000_000
]
},
%{
key: :upload_limit,
type: :integer,
description: "File size limit of uploads (except for avatar, background, banner)",
suggestions: [
16_000_000
]
},
%{
key: :avatar_upload_limit,
type: :integer,
description: "File size limit of user's profile avatars",
suggestions: [
2_000_000
]
},
%{
key: :background_upload_limit,
type: :integer,
description: "File size limit of user's profile backgrounds",
suggestions: [
4_000_000
]
},
%{
key: :banner_upload_limit,
type: :integer,
description: "File size limit of user's profile banners",
suggestions: [
4_000_000
]
},
%{
key: :poll_limits,
type: :map,
description: "A map with poll limits for local polls",
suggestions: [
%{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 31_536_000
}
],
children: [
%{
key: :max_options,
type: :integer,
description: "Maximum number of options",
suggestions: [20]
},
%{
key: :max_option_chars,
type: :integer,
description: "Maximum number of characters per option",
suggestions: [200]
},
%{
key: :min_expiration,
type: :integer,
description: "Minimum expiration time (in seconds)",
suggestions: [0]
},
%{
key: :max_expiration,
type: :integer,
description: "Maximum expiration time (in seconds)",
suggestions: [3600]
}
]
},
%{
key: :registrations_open,
type: :boolean,
description:
"Enable registrations for anyone. Invitations require this setting to be disabled."
},
%{
key: :invites_enabled,
type: :boolean,
description:
"Enable user invitations for admins (depends on `registrations_open` being disabled)"
},
%{
key: :account_activation_required,
type: :boolean,
description: "Require users to confirm their emails before signing in"
},
%{
key: :account_approval_required,
type: :boolean,
description: "Require users to be manually approved by an admin before signing in"
},
%{
key: :federating,
type: :boolean,
description: "Enable federation with other instances"
},
%{
key: :federation_incoming_replies_max_depth,
label: "Fed. incoming replies max depth",
type: :integer,
description:
"Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while" <>
" fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
suggestions: [
100
]
},
%{
key: :federation_reachability_timeout_days,
label: "Fed. reachability timeout days",
type: :integer,
description:
"Timeout (in days) of each external federation target being unreachable prior to pausing federating to it",
suggestions: [
7
]
},
%{
key: :allow_relay,
type: :boolean,
description:
"Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance."
},
%{
key: :public,
type: :boolean,
description:
"Makes the client API in authenticated mode-only except for user-profiles." <>
" Useful for disabling the Local Timeline and The Whole Known Network. " <>
" Note: when setting to `false`, please also check `:restrict_unauthenticated` setting."
},
%{
key: :quarantined_instances,
type: {:list, :tuple},
key_placeholder: "instance",
value_placeholder: "reason",
description:
"List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so",
suggestions: [
{"quarantined.com", "Reason"},
{"*.quarantined.com", "Reason"}
]
},
%{
key: :rejected_instances,
type: {:list, :tuple},
key_placeholder: "instance",
value_placeholder: "reason",
description:
"List of ActivityPub instances to reject requests from if authorized_fetch_mode is enabled",
suggestions: [
{"rejected.com", "Reason"},
{"*.rejected.com", "Reason"}
]
},
%{
key: :static_dir,
type: :string,
description: "Instance static directory",
suggestions: [
"instance/static/"
]
},
%{
key: :allowed_post_formats,
type: {:list, :string},
description: "MIME-type list of formats allowed to be posted (transformed into HTML)",
suggestions: [
"text/plain",
"text/html",
"text/markdown",
- "text/bbcode"
+ "text/bbcode",
+ "text/x.misskeymarkdown"
]
},
%{
key: :extended_nickname_format,
type: :boolean,
description:
"Enable to use extended local nicknames format (allows underscores/dashes)." <>
" This will break federation with older software for theses nicknames."
},
%{
key: :cleanup_attachments,
type: :boolean,
description: """
Enable to remove associated attachments when status is removed.
This will not affect duplicates and attachments without status.
Enabling this will increase load to database when deleting statuses on larger instances.
"""
},
%{
key: :max_pinned_statuses,
type: :integer,
description: "The maximum number of pinned statuses. 0 will disable the feature.",
suggestions: [
0,
1,
3
]
},
%{
key: :max_endorsed_users,
type: :integer,
description: "The maximum number of recommended accounts. 0 will disable the feature.",
suggestions: [
0,
1,
3
]
},
%{
key: :autofollowed_nicknames,
type: {:list, :string},
description:
"Set to nicknames of (local) users that every new user should automatically follow"
},
%{
key: :autofollowing_nicknames,
type: {:list, :string},
description:
"Set to nicknames of (local) users that automatically follows every newly registered user"
},
%{
key: :attachment_links,
type: :boolean,
description: "Enable to automatically add attachment link text to statuses"
},
%{
key: :max_report_comment_size,
type: :integer,
description: "The maximum size of the report comment. Default: 1000.",
suggestions: [
1_000
]
},
%{
key: :report_strip_status,
label: "Report strip status",
type: :boolean,
description:
"Strip associated statuses in reports to ids when closed/resolved, otherwise keep a copy"
},
%{
key: :safe_dm_mentions,
label: "Safe DM mentions",
type: :boolean,
description:
"If enabled, only mentions at the beginning of a post will be used to address people in direct messages." <>
" This is to prevent accidental mentioning of people when talking about them (e.g. \"@admin please keep an eye on @bad_actor\")." <>
" Default: disabled"
},
%{
key: :healthcheck,
type: :boolean,
description: "If enabled, system data will be shown on `/api/pleroma/healthcheck`"
},
%{
key: :remote_post_retention_days,
type: :integer,
description:
"The default amount of days to retain remote posts when pruning the database",
suggestions: [
90
]
},
%{
key: :user_bio_length,
type: :integer,
description: "A user bio maximum length. Default: 5000.",
suggestions: [
5_000
]
},
%{
key: :user_name_length,
type: :integer,
description: "A user name maximum length. Default: 100.",
suggestions: [
100
]
},
%{
key: :skip_thread_containment,
type: :boolean,
description: "Skip filtering out broken threads. Default: enabled."
},
%{
key: :limit_to_local_content,
type: {:dropdown, :atom},
description:
"Limit unauthenticated users to search for local statutes and users only. Default: `:unauthenticated`.",
suggestions: [
:unauthenticated,
:all,
false
]
},
%{
key: :max_account_fields,
type: :integer,
description: "The maximum number of custom fields in the user profile. Default: 10.",
suggestions: [
10
]
},
%{
key: :max_remote_account_fields,
type: :integer,
description:
"The maximum number of custom fields in the remote user profile. Default: 20.",
suggestions: [
20
]
},
%{
key: :account_field_name_length,
type: :integer,
description: "An account field name maximum length. Default: 512.",
suggestions: [
512
]
},
%{
key: :account_field_value_length,
type: :integer,
description: "An account field value maximum length. Default: 2048.",
suggestions: [
2048
]
},
%{
key: :registration_reason_length,
type: :integer,
description: "Maximum registration reason length. Default: 500.",
suggestions: [
500
]
},
%{
key: :external_user_synchronization,
type: :boolean,
description: "Enabling following/followers counters synchronization for external users"
},
%{
key: :multi_factor_authentication,
type: :keyword,
description: "Multi-factor authentication settings",
suggestions: [
[
totp: [digits: 6, period: 30],
backup_codes: [number: 5, length: 16]
]
],
children: [
%{
key: :totp,
label: "TOTP settings",
type: :keyword,
description: "TOTP settings",
suggestions: [digits: 6, period: 30],
children: [
%{
key: :digits,
type: :integer,
suggestions: [6],
description:
"Determines the length of a one-time pass-code, in characters. Defaults to 6 characters."
},
%{
key: :period,
type: :integer,
suggestions: [30],
description:
"A period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
}
]
},
%{
key: :backup_codes,
type: :keyword,
description: "MFA backup codes settings",
suggestions: [number: 5, length: 16],
children: [
%{
key: :number,
type: :integer,
suggestions: [5],
description: "Number of backup codes to generate."
},
%{
key: :length,
type: :integer,
suggestions: [16],
description:
"Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters."
}
]
}
]
},
%{
key: :instance_thumbnail,
type: {:string, :image},
description:
"The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance.",
suggestions: ["/instance/thumbnail.jpeg"]
},
%{
key: :favicon,
type: {:string, :image},
description: "Favicon of the instance",
suggestions: ["/favicon.png"]
},
%{
key: :show_reactions,
type: :boolean,
description: "Let favourites and emoji reactions be viewed through the API."
},
%{
key: :profile_directory,
type: :boolean,
description: "Enable profile directory."
},
%{
key: :admin_privileges,
type: {:list, :atom},
suggestions: [
:users_read,
:users_manage_invites,
:users_manage_activation_state,
:users_manage_tags,
:users_manage_credentials,
:users_delete,
:messages_read,
:messages_delete,
:instances_delete,
:reports_manage_reports,
:moderation_log_read,
:announcements_manage_announcements,
:emoji_manage_emoji,
:statistics_read
],
description:
"What extra privileges to allow admins (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)"
},
%{
key: :moderator_privileges,
type: {:list, :atom},
suggestions: [
:users_read,
:users_manage_invites,
:users_manage_activation_state,
:users_manage_tags,
:users_manage_credentials,
:users_delete,
:messages_read,
:messages_delete,
:instances_delete,
:reports_manage_reports,
:moderation_log_read,
:announcements_manage_announcements,
:emoji_manage_emoji,
:statistics_read
],
description:
"What extra privileges to allow moderators (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)"
},
%{
key: :birthday_required,
type: :boolean,
description: "Require users to enter their birthday."
},
%{
key: :birthday_min_age,
type: :integer,
description:
"Minimum required age (in days) for users to create account. Only used if birthday is required.",
suggestions: [6570]
},
%{
key: :languages,
type: {:list, :string},
description:
"Languages to be exposed in /api/v1/instance. Should be in the format of BCP47 language codes.",
suggestions: [
"en"
]
}
]
},
%{
group: :pleroma,
key: :welcome,
type: :group,
description: "Welcome messages settings",
children: [
%{
key: :direct_message,
type: :keyword,
descpiption: "Direct message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sending a direct message to newly registered users"
},
%{
key: :message,
type: :string,
description: "A message that will be sent to newly registered users",
suggestions: [
"Hi, @username! Welcome on board!"
]
},
%{
key: :sender_nickname,
type: :string,
description: "The nickname of the local user that sends a welcome message",
suggestions: [
"lain"
]
}
]
},
%{
key: :chat_message,
type: :keyword,
descpiption: "Chat message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sending a chat message to newly registered users"
},
%{
key: :message,
type: :string,
description:
"A message that will be sent to newly registered users as a chat message",
suggestions: [
"Hello, welcome on board!"
]
},
%{
key: :sender_nickname,
type: :string,
description: "The nickname of the local user that sends a welcome chat message",
suggestions: [
"lain"
]
}
]
},
%{
key: :email,
type: :keyword,
descpiption: "Email message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sending an email to newly registered users"
},
%{
key: :sender,
type: [:string, :tuple],
description:
"Email address and/or nickname that will be used to send the welcome email.",
suggestions: [
{"Pleroma App", "welcome@pleroma.app"}
]
},
%{
key: :subject,
type: :string,
description:
"Subject of the welcome email. EEX template with user and instance_name variables can be used.",
suggestions: ["Welcome to <%= instance_name%>"]
},
%{
key: :html,
type: :string,
description:
"HTML content of the welcome email. EEX template with user and instance_name variables can be used.",
suggestions: ["<h1>Hello <%= user.name%>. Welcome to <%= instance_name%></h1>"]
},
%{
key: :text,
type: :string,
description:
"Text content of the welcome email. EEX template with user and instance_name variables can be used.",
suggestions: ["Hello <%= user.name%>. \n Welcome to <%= instance_name%>\n"]
}
]
}
]
},
%{
group: :pleroma,
key: :frontend_configurations,
type: :group,
description:
"This form can be used to configure a keyword list that keeps the configuration data for any " <>
"kind of frontend. By default, settings for pleroma_fe are configured. If you want to " <>
"add your own configuration your settings all fields must be complete.",
children: [
%{
key: :pleroma_fe,
label: "Pleroma FE",
type: :map,
description: "Settings for Pleroma FE",
suggestions: [
%{
alwaysShowSubjectInput: true,
background: "/static/aurora_borealis.jpg",
collapseMessageWithSubject: false,
greentext: false,
embeddedToS: true,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "/static/img/nsfw.74818f9.png",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: true,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
subjectLineBehavior: "email",
theme: "pleroma-dark",
webPushNotifications: false
}
],
children: [
%{
key: :alwaysShowSubjectInput,
label: "Always show subject input",
type: :boolean,
description: "When disabled, auto-hide the subject field if it's empty"
},
%{
key: :background,
type: {:string, :image},
description:
"URL of the background, unless viewing a user profile with a background that is set",
suggestions: ["/images/city.jpg"]
},
%{
key: :collapseMessageWithSubject,
label: "Collapse message with subject",
type: :boolean,
description:
"When a message has a subject (aka Content Warning), collapse it by default"
},
%{
key: :greentext,
label: "Greentext",
type: :boolean,
description: "Enables green text on lines prefixed with the > character"
},
%{
key: :embeddedToS,
label: "Embedded ToS panel",
type: :boolean,
description: "Hide Terms of Service panel decorations on About and Registration pages"
},
%{
key: :hideFilteredStatuses,
label: "Hide Filtered Statuses",
type: :boolean,
description: "Hides filtered statuses from timelines"
},
%{
key: :hideMutedPosts,
label: "Hide Muted Posts",
type: :boolean,
description: "Hides muted statuses from timelines"
},
%{
key: :hidePostStats,
label: "Hide post stats",
type: :boolean,
description: "Hide notices statistics (repeats, favorites, ...)"
},
%{
key: :hideSitename,
label: "Hide Sitename",
type: :boolean,
description: "Hides instance name from PleromaFE banner"
},
%{
key: :hideUserStats,
label: "Hide user stats",
type: :boolean,
description:
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
},
%{
key: :logo,
type: {:string, :image},
description: "URL of the logo, defaults to Pleroma's logo",
suggestions: ["/static/logo.svg"]
},
%{
key: :logoMargin,
label: "Logo margin",
type: :string,
description:
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
suggestions: [".1em"]
},
%{
key: :logoMask,
label: "Logo mask",
type: :boolean,
description:
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
"If you want a colorful logo you must disable logoMask."
},
%{
key: :minimalScopesMode,
label: "Minimal scopes mode",
type: :boolean,
description:
"Limit scope selection to Direct, User default, and Scope of post replying to. " <>
"Also prevents replying to a DM with a public post from PleromaFE."
},
%{
key: :nsfwCensorImage,
label: "NSFW Censor Image",
type: {:string, :image},
description:
"URL of the image to use for hiding NSFW media attachments in the timeline",
suggestions: ["/static/img/nsfw.74818f9.png"]
},
%{
key: :postContentType,
label: "Post Content Type",
type: {:dropdown, :atom},
description: "Default post formatting option",
- suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
+ suggestions: [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode",
+ "text/x.misskeymarkdown"
+ ]
},
%{
key: :redirectRootNoLogin,
label: "Redirect root no login",
type: :string,
description:
"Relative URL which indicates where to redirect when a user isn't logged in",
suggestions: ["/main/all"]
},
%{
key: :redirectRootLogin,
label: "Redirect root login",
type: :string,
description:
"Relative URL which indicates where to redirect when a user is logged in",
suggestions: ["/main/friends"]
},
%{
key: :scopeCopy,
label: "Scope copy",
type: :boolean,
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
},
%{
key: :sidebarRight,
label: "Sidebar on Right",
type: :boolean,
description: "Change alignment of sidebar and panels to the right"
},
%{
key: :showFeaturesPanel,
label: "Show instance features panel",
type: :boolean,
description:
"Enables panel displaying functionality of the instance on the About page"
},
%{
key: :showInstanceSpecificPanel,
label: "Show instance specific panel",
type: :boolean,
description: "Whether to show the instance's custom panel"
},
%{
key: :subjectLineBehavior,
label: "Subject line behavior",
type: :string,
description: "Allows changing the default behaviour of subject lines in replies.
`email`: copy and prepend re:, as in email,
`masto`: copy verbatim, as in Mastodon,
`noop`: don't copy the subject.",
suggestions: ["email", "masto", "noop"]
},
%{
key: :theme,
type: :string,
description: "Which theme to use. Available themes are defined in styles.json",
suggestions: ["pleroma-dark"]
}
]
}
]
},
%{
group: :pleroma,
key: :assets,
type: :group,
description:
"This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend",
children: [
%{
key: :mascots,
type: {:keyword, :map},
description:
"Keyword of mascots, each element must contain both an URL and a mime_type key",
suggestions: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
]
},
%{
key: :default_mascot,
type: :atom,
description:
"This will be used as the default mascot on MastoFE. Default: `:pleroma_fox_tan`",
suggestions: [
:pleroma_fox_tan
]
},
%{
key: :default_user_avatar,
type: {:string, :image},
description: "URL of the default user avatar",
suggestions: ["/images/avi.png"]
}
]
},
%{
group: :pleroma,
key: :manifest,
type: :group,
description:
"This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.",
children: [
%{
key: :icons,
type: {:list, :map},
description: "Describe the icons of the app",
suggestion: [
%{
src: "/static/logo.png"
},
%{
src: "/static/icon.png",
type: "image/png"
},
%{
src: "/static/icon.ico",
sizes: "72x72 96x96 128x128 256x256"
}
]
},
%{
key: :theme_color,
type: :string,
description: "Describe the theme color of the app",
suggestions: ["#282c37", "mediumpurple"]
},
%{
key: :background_color,
type: :string,
description: "Describe the background color of the app",
suggestions: ["#191b22", "aliceblue"]
}
]
},
%{
group: :pleroma,
key: :media_proxy,
type: :group,
description: "Media proxy",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables proxying of remote media via the instance's proxy"
},
%{
key: :base_url,
label: "Base URL",
type: :string,
description:
"The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.",
suggestions: ["https://example.com"]
},
%{
key: :invalidation,
type: :keyword,
descpiption: "",
suggestions: [
enabled: true,
provider: Pleroma.Web.MediaProxy.Invalidation.Script
],
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables media cache object invalidation."
},
%{
key: :provider,
type: :module,
description: "Module which will be used to purge objects from the cache.",
suggestions: [
Pleroma.Web.MediaProxy.Invalidation.Script,
Pleroma.Web.MediaProxy.Invalidation.Http
]
}
]
},
%{
key: :proxy_opts,
label: "Advanced MediaProxy Options",
type: :keyword,
description: "Internal Pleroma.ReverseProxy settings",
suggestions: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
max_read_duration: 30_000
],
children: [
%{
key: :redirect_on_failure,
type: :boolean,
description: """
Redirects the client to the origin server upon encountering HTTP errors.\n
Note that files larger than Max Body Length will trigger an error. (e.g., Peertube videos)\n\n
**WARNING:** This setting will allow larger files to be accessed, but exposes the\n
IP addresses of your users to the other servers, bypassing the MediaProxy.
"""
},
%{
key: :max_body_length,
type: :integer,
description:
"Maximum file size (in bytes) allowed through the Pleroma MediaProxy cache."
},
%{
key: :max_read_duration,
type: :integer,
description: "Timeout (in milliseconds) of GET request to the remote URI."
}
]
},
%{
key: :whitelist,
type: {:list, :string},
description: "List of hosts with scheme to bypass the MediaProxy",
suggestions: ["http://example.com"]
}
]
},
%{
group: :pleroma,
key: :media_preview_proxy,
type: :group,
description: "Media preview proxy",
children: [
%{
key: :enabled,
type: :boolean,
description:
"Enables proxying of remote media preview to the instance's proxy. Requires enabled media proxy."
},
%{
key: :thumbnail_max_width,
type: :integer,
description:
"Max width of preview thumbnail for images (video preview always has original dimensions)."
},
%{
key: :thumbnail_max_height,
type: :integer,
description:
"Max height of preview thumbnail for images (video preview always has original dimensions)."
},
%{
key: :image_quality,
type: :integer,
description: "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)."
},
%{
key: :min_content_length,
type: :integer,
description:
"Min content length (in bytes) to perform preview. Media smaller in size will be served without thumbnailing."
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.MediaProxy.Invalidation.Http,
type: :group,
description: "HTTP invalidate settings",
children: [
%{
key: :method,
type: :atom,
description: "HTTP method of request. Default: :purge"
},
%{
key: :headers,
type: {:keyword, :string},
description: "HTTP headers of request",
suggestions: [{"x-refresh", 1}]
},
%{
key: :options,
type: :keyword,
description: "Request options",
children: [
%{
key: :params,
type: {:map, :string}
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.MediaProxy.Invalidation.Script,
type: :group,
description: "Invalidation script settings",
children: [
%{
key: :script_path,
type: :string,
description: "Path to executable script which will purge cached items.",
suggestions: ["./installation/nginx-cache-purge.sh.example"]
},
%{
key: :url_format,
label: "URL Format",
type: :string,
description:
"Optional URL format preprocessing. Only required for Apache's htcacheclean.",
suggestions: [":htcacheclean"]
}
]
},
%{
group: :pleroma,
key: :gopher,
type: :group,
description: "Gopher settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the gopher interface"
},
%{
key: :ip,
label: "IP",
type: :tuple,
description: "IP address to bind to",
suggestions: [{0, 0, 0, 0}]
},
%{
key: :port,
type: :integer,
description: "Port to bind to",
suggestions: [9999]
},
%{
key: :dstport,
type: :integer,
description: "Port advertised in URLs (optional, defaults to port)",
suggestions: [9999]
}
]
},
%{
group: :pleroma,
key: :activitypub,
label: "ActivityPub",
type: :group,
description: "ActivityPub-related settings",
children: [
%{
key: :unfollow_blocked,
type: :boolean,
description: "Whether blocks result in people getting unfollowed"
},
%{
key: :outgoing_blocks,
type: :boolean,
description: "Whether to federate blocks to other instances"
},
%{
key: :blockers_visible,
type: :boolean,
description: "Whether a user can see someone who has blocked them"
},
%{
key: :sign_object_fetches,
type: :boolean,
description: "Sign object fetches with HTTP signatures"
},
%{
key: :authorized_fetch_mode,
type: :boolean,
description: "Require HTTP signatures for AP fetches"
},
%{
key: :authorized_fetch_mode_exceptions,
type: {:list, :string},
description:
"List of IPs (CIDR format accepted) to exempt from HTTP Signatures requirement (for example to allow debugging, you shouldn't otherwise need this)"
},
%{
key: :note_replies_output_limit,
type: :integer,
description:
"The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)"
},
%{
key: :follow_handshake_timeout,
type: :integer,
description: "Following handshake timeout",
suggestions: [500]
},
%{
key: :client_api_enabled,
type: :boolean,
description: "Allow client to server ActivityPub interactions"
},
%{
key: :anonymize_reporter,
type: :boolean,
label: "Anonymize local reports",
description:
"If true, replace local reporters with the designated local user for the copy to be sent to remote servers"
},
%{
key: :anonymize_reporter_local_nickname,
type: :string,
label: "Anonymized reporter",
description:
"The nickname of the designated local user that replaces the actual reporter in the copy to be sent to remote servers",
suggestions: [
"lain"
]
}
]
},
%{
group: :pleroma,
key: :http_security,
label: "HTTP security",
type: :group,
description: "HTTP security settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the managed content security policy is enabled"
},
%{
key: :sts,
label: "STS",
type: :boolean,
description: "Whether to additionally send a Strict-Transport-Security header"
},
%{
key: :sts_max_age,
label: "STS max age",
type: :integer,
description: "The maximum age for the Strict-Transport-Security header if sent",
suggestions: [31_536_000]
},
%{
key: :ct_max_age,
label: "CT max age",
type: :integer,
description: "The maximum age for the Expect-CT header if sent",
suggestions: [2_592_000]
},
%{
key: :referrer_policy,
type: :string,
description: "The referrer policy to use, either \"same-origin\" or \"no-referrer\"",
suggestions: ["same-origin", "no-referrer"]
},
%{
key: :report_uri,
label: "Report URI",
type: :string,
description: "Adds the specified URL to report-uri and report-to group in CSP header",
suggestions: ["https://example.com/report-uri"]
}
]
},
%{
group: :web_push_encryption,
key: :vapid_details,
label: "Vapid Details",
type: :group,
description:
"Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it.",
children: [
%{
key: :subject,
type: :string,
description:
"A mailto link for the administrative contact." <>
" It's best if this email is not a personal email address, but rather a group email to the instance moderation team.",
suggestions: ["mailto:moderators@pleroma.com"]
},
%{
key: :public_key,
type: :string,
description: "VAPID public key",
suggestions: ["Public key"]
},
%{
key: :private_key,
type: :string,
description: "VAPID private key",
suggestions: ["Private key"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha,
type: :group,
description: "Captcha-related settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the captcha should be shown on registration"
},
%{
key: :method,
type: :module,
description: "The method/service to use for captcha",
suggestions: [Pleroma.Captcha.Kocaptcha, Pleroma.Captcha.Native]
},
%{
key: :seconds_valid,
type: :integer,
description: "The time in seconds for which the captcha is valid",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha.Kocaptcha,
type: :group,
description:
"Kocaptcha is a very simple captcha service with a single API endpoint, the source code is" <>
" here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer.",
children: [
%{
key: :endpoint,
type: :string,
description: "The kocaptcha endpoint to use",
suggestions: ["https://captcha.kotobank.ch"]
}
]
},
%{
group: :pleroma,
label: "Pleroma Admin Token",
type: :group,
description:
"Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)",
children: [
%{
key: :admin_token,
type: :string,
description: "Admin token",
suggestions: [
"Please use a high entropy string or UUID"
]
}
]
},
%{
group: :pleroma,
key: Oban,
type: :group,
description:
"[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.",
children: [
%{
key: :log,
type: {:dropdown, :atom},
description: "Logs verbose mode",
suggestions: [false, :error, :warning, :info, :debug]
},
%{
key: :queues,
type: {:keyword, :integer},
description:
"Background jobs queues (keys: queues, values: max numbers of concurrent jobs)",
suggestions: [
activity_expiration: 10,
attachments_cleanup: 5,
background: 5,
federator_incoming: 50,
federator_outgoing: 50,
mailer: 10,
scheduled_activities: 10,
transmogrifier: 20,
web_push: 50
],
children: [
%{
key: :activity_expiration,
type: :integer,
description: "Activity expiration queue",
suggestions: [10]
},
%{
key: :backup,
type: :integer,
description: "Backup queue",
suggestions: [1]
},
%{
key: :attachments_cleanup,
type: :integer,
description: "Attachment deletion queue",
suggestions: [5]
},
%{
key: :background,
type: :integer,
description: "Background queue",
suggestions: [5]
},
%{
key: :federator_incoming,
type: :integer,
description: "Incoming federation queue",
suggestions: [50]
},
%{
key: :federator_outgoing,
type: :integer,
description: "Outgoing federation queue",
suggestions: [50]
},
%{
key: :mailer,
type: :integer,
description: "Email sender queue, see Pleroma.Emails.Mailer",
suggestions: [10]
},
%{
key: :scheduled_activities,
type: :integer,
description: "Scheduled activities queue, see Pleroma.ScheduledActivities",
suggestions: [10]
},
%{
key: :transmogrifier,
type: :integer,
description: "Transmogrifier queue",
suggestions: [20]
},
%{
key: :web_push,
type: :integer,
description: "Web push notifications queue",
suggestions: [50]
}
]
},
%{
key: :crontab,
type: {:list, :tuple},
description: "Settings for cron background jobs",
suggestions: [
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Metadata,
type: :group,
description: "Metadata-related settings",
children: [
%{
key: :providers,
type: {:list, :module},
description: "List of metadata providers to enable",
suggestions: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.Feed
]
},
%{
key: :unfurl_nsfw,
label: "Unfurl NSFW",
type: :boolean,
description: "When enabled NSFW attachments will be shown in previews"
}
]
},
%{
group: :pleroma,
key: :rich_media,
type: :group,
description:
"If enabled the instance will parse metadata from attached links to generate link previews",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables RichMedia parsing of URLs"
},
%{
key: :ignore_hosts,
type: {:list, :string},
description: "List of hosts which will be ignored by the metadata parser",
suggestions: ["accounts.google.com", "xss.website"]
},
%{
key: :ignore_tld,
label: "Ignore TLD",
type: {:list, :string},
description: "List TLDs (top-level domains) which will ignore for parse metadata",
suggestions: ["local", "localdomain", "lan"]
},
%{
key: :parsers,
type: {:list, :module},
description:
"List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parsers.OEmbed,
Pleroma.Web.RichMedia.Parsers.TwitterCard
]
},
%{
key: :ttl_setters,
label: "TTL setters",
type: {:list, :module},
description:
"List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
]
},
%{
key: :timeout,
type: :integer,
description:
"Amount of milliseconds after which the HTTP request is forcibly terminated.",
suggestions: [5_000]
},
%{
key: :user_agent,
type: :string,
description: "Custom User-Agent header to be used when fetching rich media content."
}
]
},
%{
group: :pleroma,
key: Pleroma.Formatter,
label: "Linkify",
type: :group,
description:
"Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.",
children: [
%{
key: :class,
type: [:string, :boolean],
description: "Specify the class to be added to the generated link. Disable to clear.",
suggestions: ["auto-linker", false]
},
%{
key: :rel,
type: [:string, :boolean],
description: "Override the rel attribute. Disable to clear.",
suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
type: :boolean,
description: "Link URLs will open in a new window/tab."
},
%{
key: :truncate,
type: [:integer, :boolean],
description:
"Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`",
suggestions: [15, false]
},
%{
key: :strip_prefix,
type: :boolean,
description: "Strip the scheme prefix."
},
%{
key: :extra,
type: :boolean,
description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
},
%{
key: :validate_tld,
type: [:atom, :boolean],
description:
"Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)",
suggestions: [:no_scheme, true]
}
]
},
%{
group: :pleroma,
key: Pleroma.ScheduledActivity,
type: :group,
description: "Scheduled activities settings",
children: [
%{
key: :daily_user_limit,
type: :integer,
description:
"The number of scheduled activities a user is allowed to create in a single day. Default: 25.",
suggestions: [25]
},
%{
key: :total_user_limit,
type: :integer,
description:
"The number of scheduled activities a user is allowed to create in total. Default: 300.",
suggestions: [300]
},
%{
key: :enabled,
type: :boolean,
description: "Whether scheduled activities are sent to the job queue to be executed"
}
]
},
%{
group: :pleroma,
key: Pleroma.Workers.PurgeExpiredActivity,
type: :group,
description: "Expired activities settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables expired activities addition & deletion"
},
%{
key: :min_lifetime,
type: :integer,
description: "Minimum lifetime for ephemeral activity (in seconds)",
suggestions: [600]
}
]
},
%{
group: :pleroma,
label: "Pleroma Authenticator",
type: :group,
description: "Authenticator",
children: [
%{
key: Pleroma.Web.Auth.Authenticator,
type: :module,
suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
}
]
},
%{
group: :pleroma,
key: :ldap,
label: "LDAP",
type: :group,
description:
"Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password" <>
" will be verified by trying to authenticate (bind) to a LDAP server." <>
" If a user exists in the LDAP directory but there is no account with the same name yet on the" <>
" Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name.",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables LDAP authentication"
},
%{
key: :host,
type: :string,
description: "LDAP server hostname",
suggestions: ["localhosts"]
},
%{
key: :port,
type: :integer,
description: "LDAP port, e.g. 389 or 636",
suggestions: [389, 636]
},
%{
key: :ssl,
label: "SSL",
type: :boolean,
description: "Enable to use SSL, usually implies the port 636"
},
%{
key: :sslopts,
label: "SSL options",
type: :keyword,
description: "Additional SSL options",
suggestions: [verify: :verify_peer],
children: [
%{
key: :verify,
type: :atom,
description: "Type of cert verification",
suggestions: [:verify_peer]
}
]
},
%{
key: :tls,
label: "TLS",
type: :boolean,
description: "Enable to use STARTTLS, usually implies the port 389"
},
%{
key: :tlsopts,
label: "TLS options",
type: :keyword,
description: "Additional TLS options",
suggestions: [verify: :verify_peer],
children: [
%{
key: :verify,
type: :atom,
description: "Type of cert verification",
suggestions: [:verify_peer]
}
]
},
%{
key: :base,
type: :string,
description: "LDAP base, e.g. \"dc=example,dc=com\"",
suggestions: ["dc=example,dc=com"]
},
%{
key: :uid,
label: "UID Attribute",
type: :string,
description:
"LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"",
suggestions: ["cn"]
},
%{
key: :cacertfile,
label: "CACertfile",
type: :string,
description: "Path to CA certificate file"
},
%{
key: :mail,
label: "Mail Attribute",
type: :string,
description:
"LDAP attribute name to use as the email address when automatically registering the user on first login",
suggestions: ["mail"]
}
]
},
%{
group: :pleroma,
key: :auth,
type: :group,
description: "Authentication / authorization settings",
children: [
%{
key: :enforce_oauth_admin_scope_usage,
label: "Enforce OAuth admin scope usage",
type: :boolean,
description:
"OAuth admin scope requirement toggle. " <>
"If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
"(client app must support admin scopes). If disabled and token doesn't have admin scope(s), " <>
"`is_admin` user flag grants access to admin-specific actions."
},
%{
key: :auth_template,
type: :string,
description:
"Authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`.",
suggestions: ["show.html"]
},
%{
key: :oauth_consumer_template,
label: "OAuth consumer template",
type: :string,
description:
"OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to" <>
" `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.",
suggestions: ["consumer.html"]
},
%{
key: :oauth_consumer_strategies,
label: "OAuth consumer strategies",
type: {:list, :string},
description:
"The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
" Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\"" <>
" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>).",
suggestions: ["twitter", "keycloak:ueberauth_keycloak_strategy"]
}
]
},
%{
group: :pleroma,
key: :email_notifications,
type: :group,
description: "Email notifications settings",
children: [
%{
key: :digest,
type: :map,
description:
"emails of \"what you've missed\" for users who have been inactive for a while",
suggestions: [
%{
active: false,
schedule: "0 0 * * 0",
interval: 7,
inactivity_threshold: 7
}
],
children: [
%{
key: :active,
label: "Enabled",
type: :boolean,
description: "Globally enable or disable digest emails"
},
%{
key: :schedule,
type: :string,
description:
"When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\".",
suggestions: ["0 0 * * 0"]
},
%{
key: :interval,
type: :integer,
description: "Minimum interval between digest emails to one user",
suggestions: [7]
},
%{
key: :inactivity_threshold,
type: :integer,
description: "Minimum user inactivity threshold",
suggestions: [7]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.UserEmail,
type: :group,
description: "Email template settings",
children: [
%{
key: :logo,
type: {:string, :image},
description: "A path to a custom logo. Set it to `nil` to use the default Pleroma logo.",
suggestions: ["some/path/logo.png"]
},
%{
key: :styling,
type: :map,
description: "A map with color settings for email templates.",
suggestions: [
%{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
],
children: [
%{
key: :link_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :background_color,
type: :string,
suggestions: ["#2C3645"]
},
%{
key: :content_background_color,
type: :string,
suggestions: ["#1B2635"]
},
%{
key: :header_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :text_color,
type: :string,
suggestions: ["#b9b9ba"]
},
%{
key: :text_muted_color,
type: :string,
suggestions: ["#b9b9ba"]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.NewUsersDigestEmail,
type: :group,
description: "New users admin email digest",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables new users admin digest email when `true`"
}
]
},
%{
group: :pleroma,
key: :oauth2,
label: "OAuth2",
type: :group,
description: "Configure OAuth 2 provider capabilities",
children: [
%{
key: :token_expires_in,
type: :integer,
description: "The lifetime in seconds of the access token",
suggestions: [2_592_000]
},
%{
key: :issue_new_refresh_token,
type: :boolean,
description:
"Keeps old refresh token or generate new refresh token when to obtain an access token"
},
%{
key: :clean_expired_tokens,
type: :boolean,
description: "Enable a background job to clean expired OAuth tokens. Default: disabled."
}
]
},
%{
group: :pleroma,
key: :emoji,
type: :group,
children: [
%{
key: :shortcode_globs,
type: {:list, :string},
description: "Location of custom emoji files. * can be used as a wildcard.",
suggestions: ["/emoji/custom/**/*.png"]
},
%{
key: :pack_extensions,
type: {:list, :string},
description:
"A list of file extensions for emojis, when no emoji.txt for a pack is present",
suggestions: [".png", ".gif"]
},
%{
key: :groups,
type: {:keyword, {:list, :string}},
description:
"Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name" <>
" and the value is the location or array of locations. * can be used as a wildcard.",
suggestions: [
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
]
},
%{
key: :default_manifest,
type: :string,
description:
"Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download." <>
" Currently only one manifest can be added (no arrays).",
suggestions: ["https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"]
},
%{
key: :shared_pack_cache_seconds_per_file,
label: "Shared pack cache s/file",
type: :integer,
descpiption:
"When an emoji pack is shared, the archive is created and cached in memory" <>
" for this amount of seconds multiplied by the number of files.",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: :rate_limit,
type: :group,
description:
"Rate limit settings. This is an advanced feature enabled only for :authentication by default.",
children: [
%{
key: :search,
type: [:tuple, {:list, :tuple}],
description: "For the search requests (account & status search etc.)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :timeline,
type: [:tuple, {:list, :tuple}],
description: "For requests to timelines (each timeline has it's own limiter)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :app_account_creation,
type: [:tuple, {:list, :tuple}],
description: "For registering user accounts from the same IP address",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relations_actions,
type: [:tuple, {:list, :tuple}],
description: "For actions on relationships with all users (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relation_id_action,
label: "Relation ID action",
type: [:tuple, {:list, :tuple}],
description: "For actions on relation with a specific user (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :statuses_actions,
type: [:tuple, {:list, :tuple}],
description:
"For create / delete / fav / unfav / reblog / unreblog actions on any statuses",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :status_id_action,
label: "Status ID action",
type: [:tuple, {:list, :tuple}],
description:
"For fav / unfav or reblog / unreblog actions on the same status by the same user",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :authentication,
type: [:tuple, {:list, :tuple}],
description: "For authentication create / password check / user existence check requests",
suggestions: [{60_000, 15}]
}
]
},
%{
group: :mime,
label: "Mime Types",
type: :group,
description: "Mime Types settings",
children: [
%{
key: :types,
type: :map,
suggestions: [
%{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
],
children: [
%{
key: "application/xml",
type: {:list, :string},
suggestions: ["xml"]
},
%{
key: "application/xrd+xml",
type: {:list, :string},
suggestions: ["xrd+xml"]
},
%{
key: "application/jrd+json",
type: {:list, :string},
suggestions: ["jrd+json"]
},
%{
key: "application/activity+json",
type: {:list, :string},
suggestions: ["activity+json"]
},
%{
key: "application/ld+json",
type: {:list, :string},
suggestions: ["activity+json"]
}
]
}
]
},
%{
group: :pleroma,
key: :shout,
type: :group,
description: "Pleroma shout settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the backend Shoutbox chat feature."
},
%{
key: :limit,
type: :integer,
description: "Shout message character limit.",
suggestions: [
5_000
]
}
]
},
%{
group: :pleroma,
key: :http,
label: "HTTP",
type: :group,
description: "HTTP settings",
children: [
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["localhost:9020", {:socks5, :localhost, 3090}]
},
%{
key: :send_user_agent,
type: :boolean
},
%{
key: :user_agent,
type: [:string, :atom],
description:
"What user agent to use. Must be a string or an atom `:default`. Default value is `:default`.",
suggestions: ["Pleroma", :default]
},
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
suggestions: [],
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS version to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2", ":tlsv1.3"]
}
]
}
]
}
]
},
%{
group: :pleroma,
key: :markup,
label: "Markup Settings",
type: :group,
children: [
%{
key: :allow_inline_images,
type: :boolean
},
%{
key: :allow_headings,
type: :boolean
},
%{
key: :allow_tables,
type: :boolean
},
%{
key: :allow_fonts,
type: :boolean
},
%{
key: :scrub_policy,
type: {:list, :module},
description:
"Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name.",
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
}
]
},
%{
group: :pleroma,
key: :user,
type: :group,
children: [
%{
key: :deny_follow_blocked,
type: :boolean
}
]
},
%{
group: :pleroma,
key: Pleroma.User,
type: :group,
children: [
%{
key: :restricted_nicknames,
type: {:list, :string},
description: "List of nicknames users may not register with.",
suggestions: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web"
]
},
%{
key: :email_blacklist,
type: {:list, :string},
description: "List of email domains users may not register with.",
suggestions: ["mailinator.com", "maildrop.cc"]
}
]
},
%{
group: :cors_plug,
label: "CORS plug config",
type: :group,
children: [
%{
key: :max_age,
type: :integer,
suggestions: [86_400]
},
%{
key: :methods,
type: {:list, :string},
suggestions: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]
},
%{
key: :expose,
type: {:list, :string},
suggestions: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
]
},
%{
key: :credentials,
type: :boolean
},
%{
key: :headers,
type: {:list, :string},
suggestions: ["Authorization", "Content-Type", "Idempotency-Key"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Plugs.RemoteIp,
type: :group,
description: """
- `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+ `Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration.
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
""",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enable/disable the plug. Default: disabled."
},
%{
key: :headers,
type: {:list, :string},
description: """
A list of strings naming the HTTP headers to use when deriving the true client IP. Default: `["x-forwarded-for"]`.
"""
},
+ %{
+ key: :clients,
+ type: {:list, :string},
+ description:
+ "A list of client IPs or subnets in CIDR notation. These will not be treated as proxies or reserved ranges. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128."
+ },
%{
key: :proxies,
type: {:list, :string},
description:
"A list of upstream proxy IP subnets in CIDR notation from which we will parse the content of `headers`. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128."
},
%{
key: :reserved,
type: {:list, :string},
description: """
A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `["127.0.0.0/8", "::1/128", "fc00::/7", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]`
"""
}
]
},
%{
group: :pleroma,
key: :web_cache_ttl,
label: "Web cache TTL",
type: :group,
description:
"The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.",
children: [
%{
key: :activity_pub,
type: :integer,
description:
"Activity pub routes (except question activities). Default: `nil` (no expiration).",
suggestions: [nil]
},
%{
key: :activity_pub_question,
type: :integer,
description: "Activity pub routes (question activities). Default: `30_000` (30 seconds).",
suggestions: [30_000]
}
]
},
%{
group: :pleroma,
key: :static_fe,
label: "Static FE",
type: :group,
description:
"Render profiles and posts using server-generated HTML that is viewable without using JavaScript",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the rendering of static HTML. Default: disabled."
}
]
},
%{
group: :pleroma,
key: :feed,
type: :group,
description: "Configure feed rendering",
children: [
%{
key: :post_title,
type: :map,
description: "Configure title rendering",
children: [
%{
key: :max_length,
type: :integer,
description: "Maximum number of characters before truncating title",
suggestions: [100]
},
%{
key: :omission,
type: :string,
description: "Replacement which will be used after truncating string",
suggestions: ["..."]
}
]
}
]
},
%{
group: :pleroma,
key: :mrf_follow_bot,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.FollowBotPolicy",
label: "MRF FollowBot Policy",
type: :group,
description: "Automatically follows newly discovered accounts.",
children: [
%{
key: :follower_nickname,
type: :string,
description: "The name of the bot account to use for following newly discovered users.",
suggestions: ["followbot"]
}
]
},
%{
group: :pleroma,
key: :modules,
type: :group,
description: "Custom Runtime Modules",
children: [
%{
key: :runtime_dir,
type: :string,
description: "A path to custom Elixir modules (such as MRF policies)."
}
]
},
%{
group: :pleroma,
key: :streamer,
type: :group,
description: "Settings for notifications streamer",
children: [
%{
key: :workers,
type: :integer,
description: "Number of workers to send notifications",
suggestions: [3]
},
%{
key: :overflow_workers,
type: :integer,
description: "Maximum number of workers created if pool is empty",
suggestions: [2]
}
]
},
%{
group: :pleroma,
key: :connections_pool,
type: :group,
description: "Advanced settings for `Gun` connections pool",
children: [
%{
key: :connection_acquisition_wait,
type: :integer,
description:
"Timeout to acquire a connection from pool. The total max time is this value multiplied by the number of retries. Default: 250ms.",
suggestions: [250]
},
%{
key: :connection_acquisition_retries,
type: :integer,
description:
"Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5",
suggestions: [5]
},
%{
key: :max_connections,
type: :integer,
description: "Maximum number of connections in the pool. Default: 250 connections.",
suggestions: [250]
},
%{
key: :connect_timeout,
type: :integer,
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
suggestions: [5000]
},
%{
key: :reclaim_multiplier,
type: :integer,
description:
"Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1",
suggestions: [0.1]
}
]
},
%{
group: :pleroma,
key: :pools,
type: :group,
description: "Advanced settings for `Gun` workers pools",
children:
Enum.map([:federation, :media, :upload, :default], fn pool_name ->
%{
key: pool_name,
type: :keyword,
description: "Settings for #{pool_name} pool.",
children: [
%{
key: :size,
type: :integer,
description: "Maximum number of concurrent requests in the pool.",
suggestions: [50]
},
%{
key: :max_waiting,
type: :integer,
description:
"Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errors when a new request is made",
suggestions: [10]
},
%{
key: :recv_timeout,
type: :integer,
description: "Timeout for the pool while gun will wait for response",
suggestions: [10_000]
}
]
}
end)
},
%{
group: :pleroma,
key: :hackney_pools,
type: :group,
description: "Advanced settings for `Hackney` connections pools",
children: [
%{
key: :federation,
type: :keyword,
description: "Settings for federation pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :media,
type: :keyword,
description: "Settings for media pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [50]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [150_000]
}
]
},
%{
key: :upload,
type: :keyword,
description: "Settings for upload pool.",
children: [
%{
key: :max_connections,
type: :integer,
description: "Number workers in the pool.",
suggestions: [25]
},
%{
key: :timeout,
type: :integer,
description: "Timeout while `hackney` will wait for response.",
suggestions: [300_000]
}
]
}
]
},
%{
group: :pleroma,
key: :restrict_unauthenticated,
label: "Restrict Unauthenticated",
type: :group,
description:
"Disallow viewing timelines, user profiles and statuses for unauthenticated users.",
children: [
%{
key: :timelines,
type: :map,
description: "Settings for public and federated timelines.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view public timeline."
},
%{
key: :federated,
type: :boolean,
description: "Disallow view federated timeline."
}
]
},
%{
key: :profiles,
type: :map,
description: "Settings for user profiles.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local user profiles."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote user profiles."
}
]
},
%{
key: :activities,
type: :map,
description: "Settings for statuses.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local statuses."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote statuses."
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.ApiSpec.CastAndValidate,
type: :group,
children: [
%{
key: :strict,
type: :boolean,
description:
"Enables strict input validation (useful in development, not recommended in production)"
}
]
},
%{
group: :pleroma,
key: :instances_favicons,
type: :group,
description: "Control favicons for instances",
children: [
%{
key: :enabled,
type: :boolean,
description: "Allow/disallow displaying and getting instances favicons"
}
]
},
%{
group: :ex_aws,
key: :s3,
type: :group,
descriptions: "S3 service related settings",
children: [
%{
key: :access_key_id,
type: :string,
description: "S3 access key ID",
suggestions: ["AKIAQ8UKHTGIYN7DMWWJ"]
},
%{
key: :secret_access_key,
type: :string,
description: "Secret access key",
suggestions: ["JFGt+fgH1UQ7vLUQjpW+WvjTdV/UNzVxcwn7DkaeFKtBS5LvoXvIiME4NQBsT6ZZ"]
},
%{
key: :host,
type: :string,
description: "S3 host",
suggestions: ["s3.eu-central-1.amazonaws.com"]
},
%{
key: :region,
type: :string,
description: "S3 region (for AWS)",
suggestions: ["us-east-1"]
}
]
},
%{
group: :pleroma,
key: :frontends,
type: :group,
description: "Installed frontends management",
children: [
%{
key: :primary,
type: :map,
description: "Primary frontend, the one that is served for all pages by default",
children: installed_frontend_options
},
%{
key: :admin,
type: :map,
description: "Admin frontend",
children: installed_frontend_options
},
%{
key: :available,
type: :map,
description:
"A map containing available frontends and parameters for their installation.",
children: frontend_options
},
%{
key: :pickable,
type: {:list, :string},
description:
"A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable."
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Preload,
type: :group,
description: "Preload-related settings",
children: [
%{
key: :providers,
type: {:list, :module},
description: "List of preload providers to enable",
suggestions: [
Pleroma.Web.Preload.Providers.Instance,
Pleroma.Web.Preload.Providers.User,
Pleroma.Web.Preload.Providers.Timelines
]
}
]
},
%{
group: :pleroma,
key: :majic_pool,
type: :group,
description: "Majic/libmagic configuration",
children: [
%{
key: :size,
type: :integer,
description: "Number of majic workers to start.",
suggestions: [2]
}
]
},
%{
group: :pleroma,
key: Pleroma.User.Backup,
type: :group,
description: "Account Backup",
children: [
%{
key: :purge_after_days,
type: :integer,
description: "Remove backup archives after N days",
suggestions: [30]
},
%{
key: :limit_days,
type: :integer,
description: "Limit user to export not more often than once per N days",
suggestions: [7]
},
%{
key: :process_chunk_size,
type: :integer,
label: "Process Chunk Size",
description: "The number of activities to fetch in the backup job for each chunk.",
suggestions: [100]
},
%{
key: :timeout,
type: :integer,
label: "Timeout",
description: "The amount of time to wait for backup to complete in seconds.",
suggestions: [1_800]
}
]
},
%{
group: :prometheus,
key: Pleroma.Web.Endpoint.MetricsExporter,
type: :group,
description: "Prometheus app metrics endpoint configuration",
children: [
%{
key: :enabled,
type: :boolean,
description: "[Pleroma extension] Enables app metrics endpoint."
},
%{
key: :ip_whitelist,
label: "IP Whitelist",
type: [{:list, :string}, {:list, :charlist}, {:list, :tuple}],
description: "Restrict access of app metrics endpoint to the specified IP addresses."
},
%{
key: :auth,
type: [:boolean, :tuple],
description: "Enables HTTP Basic Auth for app metrics endpoint.",
suggestion: [false, {:basic, "myusername", "mypassword"}]
},
%{
key: :path,
type: :string,
description: "App metrics endpoint URI path.",
suggestions: ["/api/pleroma/app_metrics"]
},
%{
key: :format,
type: :atom,
description: "App metrics endpoint output format.",
suggestions: [:text, :protobuf]
}
]
},
%{
group: :pleroma,
key: ConcurrentLimiter,
type: :group,
description: "Limits configuration for background tasks.",
children: [
%{
key: Pleroma.Web.RichMedia.Helpers,
type: :keyword,
description: "Concurrent limits configuration for getting RichMedia for activities.",
suggestions: [max_running: 5, max_waiting: 5],
children: [
%{
key: :max_running,
type: :integer,
description: "Max running concurrently jobs.",
suggestion: [5]
},
%{
key: :max_waiting,
type: :integer,
description: "Max waiting jobs.",
suggestion: [5]
}
]
},
%{
key: Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy,
type: :keyword,
description: "Concurrent limits configuration for MediaProxyWarmingPolicy.",
suggestions: [max_running: 5, max_waiting: 5],
children: [
%{
key: :max_running,
type: :integer,
description: "Max running concurrently jobs.",
suggestion: [5]
},
%{
key: :max_waiting,
type: :integer,
description: "Max waiting jobs.",
suggestion: [5]
}
]
},
%{
key: Pleroma.Webhook.Notify,
type: :keyword,
description: "Concurrent limits configuration for webhooks.",
suggestions: [max_running: 5, max_waiting: 5],
children: [
%{
key: :max_running,
type: :integer,
description: "Max running concurrently jobs.",
suggestion: [5]
},
%{
key: :max_waiting,
type: :integer,
description: "Max waiting jobs.",
suggestion: [5]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Search,
type: :group,
description: "General search settings.",
children: [
%{
key: :module,
type: :keyword,
description: "Selected search module.",
suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch]
}
]
},
%{
group: :pleroma,
key: Pleroma.Search.Meilisearch,
type: :group,
description: "Meilisearch settings.",
children: [
%{
key: :url,
type: :string,
description: "Meilisearch URL.",
suggestion: ["http://127.0.0.1:7700/"]
},
%{
key: :private_key,
type: :string,
description:
"Private key for meilisearch authentication, or `nil` to disable private key authentication.",
suggestion: [nil]
},
%{
key: :initial_indexing_chunk_size,
type: :integer,
description:
"Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
" since there's a limit on maximum insert size",
suggestion: [100_000]
}
]
},
%{
group: :pleroma,
key: Pleroma.Language.LanguageDetector,
type: :group,
description: "Language detection providers",
children: [
%{
key: :provider,
type: :module,
suggestions: {:list_behaviour_implementations, Pleroma.Language.LanguageDetector.Provider}
},
%{
group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext},
key: :model,
label: "fastText language detection model",
type: :string,
suggestions: ["/usr/share/fasttext/lid.176.bin"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Language.Translation,
type: :group,
description: "Translation providers",
children: [
%{
key: :provider,
type: :module,
suggestions: {:list_behaviour_implementations, Pleroma.Language.Translation.Provider}
},
%{
group: {:subgroup, Pleroma.Language.Translation.Deepl},
key: :base_url,
label: "DeepL base URL",
type: :string,
suggestions: ["https://api-free.deepl.com", "https://api.deepl.com"]
},
%{
group: {:subgroup, Pleroma.Language.Translation.Deepl},
key: :api_key,
label: "DeepL API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
key: :base_url,
label: "LibreTranslate instance URL",
type: :string,
suggestions: ["https://libretranslate.com"]
},
%{
group: {:subgroup, Pleroma.Language.Translation.Libretranslate},
key: :api_key,
label: "LibreTranslate API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
},
%{
group: {:subgroup, Pleroma.Language.Translation.TranslateLocally},
key: :intermediary_language,
label:
"translateLocally intermediary language (used when direct source->target model is not available)",
type: :string,
suggestions: ["en"]
},
%{
group: {:subgroup, Pleroma.Language.Translation.Mozhi},
key: :base_url,
label: "Mozhi instance URL",
type: :string
},
%{
group: {:subgroup, Pleroma.Language.Translation.Mozhi},
key: :engine,
label: "Engine used for Mozhi",
type: :string,
suggestions: ["libretranslate"]
}
]
}
]
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 11d5af2fb..4bf2f6b95 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -1,161 +1,172 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
alias Pleroma.HTML
alias Pleroma.User
@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
defp linkify_opts do
Pleroma.Config.get(Pleroma.Formatter) ++
[
hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
]
end
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
%User{} ->
# escape markdown characters with `\\`
# (we don't want something like @user__name to be parsed by markdown)
String.replace(mention, @markdown_characters_regex, "\\\\\\1")
_ ->
buffer
end
end
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%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}"
link =
Phoenix.HTML.Tag.content_tag(:a, tag_text,
class: "hashtag",
"data-tag": tag,
href: url,
rel: "tag ugc"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
@doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
"""
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = linkify_opts() ++ options
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
end
@doc """
Escapes a special characters in mention names.
"""
def mentions_escape(text, options \\ []) do
options =
Keyword.merge(options,
mention: true,
url: false,
mention_handler: &Pleroma.Formatter.escape_mention_handler/4
)
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
Linkify.link(mentions, options) <> Linkify.link(rest, options)
else
Linkify.link(text, options)
end
end
def markdown_to_html(text) do
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end
+ def markdown_to_html(text, opts) do
+ Earmark.as_html!(
+ text,
+ %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
+ )
+ end
+
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
def html_escape(text, "text/html") do
HTML.filter_tags(text)
end
+ def html_escape(text, "text/x.misskeymarkdown") do
+ HTML.filter_tags(text)
+ end
+
def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
part
end)
|> Enum.join("")
end
def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
if String.length(text) < max_length do
text
else
length_with_omission = max_length - String.length(omission)
String.slice(text, 0, length_with_omission) <> omission
end
end
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
end
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
index e651d7d9d..e37a3fefe 100644
--- a/lib/pleroma/frontend.ex
+++ b/lib/pleroma/frontend.ex
@@ -1,97 +1,97 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Frontend do
alias Pleroma.Config
require Logger
def install(name, opts \\ []) do
frontend_info = %{
"ref" => opts[:ref],
"build_url" => opts[:build_url],
"build_dir" => opts[:build_dir]
}
frontend_info =
[:frontends, :available, name]
|> Config.get(%{})
|> Map.merge(frontend_info, fn _key, config, cmd ->
# This only overrides things that are actually set
cmd || config
end)
ref = frontend_info["ref"]
unless ref do
raise "No ref given or configured"
end
dest = Path.join([dir(), name, ref])
label = "#{name} (#{ref})"
tmp_dir = Path.join(dir(), "tmp")
with {_, :ok} <-
{:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, opts[:file])},
Logger.info("Installing #{label} to #{dest}"),
:ok <- install_frontend(frontend_info, tmp_dir, dest) do
File.rm_rf!(tmp_dir)
Logger.info("Frontend #{label} installed to #{dest}")
else
{:download_or_unzip, _} ->
Logger.info("Could not download or unzip the frontend")
{:error, "Could not download or unzip the frontend"}
end
end
def dir(opts \\ []) do
if is_nil(opts[:static_dir]) do
Pleroma.Config.get!([:instance, :static_dir])
else
opts[:static_dir]
end
|> Path.join("frontends")
end
defp download_or_unzip(frontend_info, temp_dir, nil),
do: download_build(frontend_info, temp_dir)
defp download_or_unzip(_frontend_info, temp_dir, file) do
with {:ok, zip} <- File.read(Path.expand(file)) do
unzip(zip, temp_dir)
end
end
def unzip(zip, dest) do
File.rm_rf!(dest)
Pleroma.Backports.mkdir_p!(dest)
case Pleroma.SafeZip.unzip_data(zip, dest) do
{:ok, _} -> :ok
error -> error
end
end
defp download_build(frontend_info, dest) do
- Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
+ Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}")
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
unzip(zip_body, dest)
else
{:error, e} -> {:error, e}
e -> {:error, e}
end
end
defp install_frontend(frontend_info, source, dest) do
from = frontend_info["build_dir"] || "dist"
File.rm_rf!(dest)
Pleroma.Backports.mkdir_p!(dest)
File.cp_r!(Path.join([source, from]), dest)
:ok
end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index f1e07a257..5e9314446 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,469 +1,477 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Workers.AttachmentsCleanupWorker
require Logger
@type t() :: %__MODULE__{}
@derive {Jason.Encoder, only: [:data]}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "objects" do
field(:data, :map)
many_to_many(:hashtags, Hashtag, join_through: "hashtags_objects", on_replace: :delete)
timestamps()
end
def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
object_position = Map.get(query.aliases, :object, 0)
join(query, join_type, [{object, object_position}], a in Activity,
on:
fragment(
"associated_object_id(?) = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
object.data,
a.data,
^activity_type
),
as: :object_activity
)
end
def create(data) do
%Object{}
|> Object.change(%{data: data})
|> Repo.insert()
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
# Expecting `maybe_handle_hashtags_change/1` to run last:
|> maybe_handle_hashtags_change(struct)
end
# Note: not checking activity type (assuming non-legacy objects are associated with Create act.)
defp maybe_handle_hashtags_change(changeset, struct) do
with %Ecto.Changeset{valid?: true} <- changeset,
data_hashtags_change = get_change(changeset, :data),
{_, true} <- {:changed, hashtags_changed?(struct, data_hashtags_change)},
{:ok, hashtag_records} <-
data_hashtags_change
|> object_data_hashtags()
|> Hashtag.get_or_create_by_names() do
put_assoc(changeset, :hashtags, hashtag_records)
else
%{valid?: false} ->
changeset
{:changed, false} ->
changeset
{:error, _} ->
validate_change(changeset, :data, fn _, _ ->
[data: "error referencing hashtags"]
end)
end
end
defp hashtags_changed?(%Object{} = struct, %{"tag" => _} = data) do
Enum.sort(embedded_hashtags(struct)) !=
Enum.sort(object_data_hashtags(data))
end
defp hashtags_changed?(_, _), do: false
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
@doc """
Get a single attachment by it's name and href
"""
@spec get_attachment_by_name_and_href(String.t(), String.t()) :: Object.t() | nil
def get_attachment_by_name_and_href(name, href) do
query =
from(o in Object,
where: fragment("(?)->>'name' = ?", o.data, ^name),
where: fragment("(?)->>'href' = ?", o.data, ^href)
)
Repo.one(query)
end
defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
|> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
def normalize(_, options \\ [fetch: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _), do: object
def normalize(%Activity{object: %Object{} = object}, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, options)
end
# No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, options) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, options)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
def normalize(ap_id, options) when is_binary(ap_id) do
cond do
Keyword.get(options, :fetch) ->
case Fetcher.fetch_object_from_id(ap_id, options) do
{:ok, object} -> object
_ -> nil
end
true ->
get_cached_by_ap_id(ap_id)
end
end
def normalize(_, _), do: nil
# Owned objects can only be accessed by their owner
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
if actor == ap_id do
:ok
else
{:error, :forbidden}
end
end
# Legacy objects can be accessed by anybody
def authorize_access(%Object{}, %User{}), do: :ok
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
with {:ok, nil} <- @cachex.get(:object_cache, key),
object when not is_nil(object) <- get_by_ap_id(ap_id),
{:ok, true} <- @cachex.put(:object_cache, key, object) do
object
else
{:ok, object} -> object
nil -> nil
end
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
formerType: type,
deleted: deleted
}
|> Map.from_struct()
end
def swap_object_with_tombstone(object) do
tombstone = make_tombstone(object)
with {:ok, object} <-
object
|> Object.change(%{data: tombstone})
|> Repo.update() do
Hashtag.unlink(object)
{:ok, object}
end
end
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
{:ok, _} <- invalid_object_cache(object) do
cleanup_attachments(
Config.get([:instance, :cleanup_attachments]),
object
)
{:ok, object, deleted_activity}
end
end
@spec cleanup_attachments(boolean(), Object.t()) ::
{:ok, Oban.Job.t() | nil}
def cleanup_attachments(true, %Object{} = object) do
AttachmentsCleanupWorker.new(%{"op" => "cleanup_attachments", "object" => object})
|> Oban.insert()
end
def cleanup_attachments(_, _), do: {:ok, nil}
def prune(%Object{data: %{"id" => _id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, _} <- invalid_object_cache(object) do
{:ok, object}
end
end
def invalid_object_cache(%Object{data: %{"id" => id}}) do
with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
@cachex.del(:web_resp_cache, URI.parse(id).path)
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
@cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
end
def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do
set_cache(object)
end
end
def increase_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
defp poll_is_multiple?(%Object{data: %{"anyOf" => [_ | _]}}), do: true
defp poll_is_multiple?(_), do: false
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def increase_quotes_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{quotesCount}',
(coalesce((?->>'quotesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def decrease_quotes_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
safe_jsonb_set(?, '{quotesCount}',
(greatest(0, (?->>'quotesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id, fetch: false),
"Question" <- object.data["type"] do
key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
options =
object.data[key]
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
- voters = [actor | object.data["voters"] || []] |> Enum.uniq()
+ existing_voters = object.data["voters"] || []
+ voters = [actor | existing_voters] |> Enum.uniq()
+ new_voter? = actor not in existing_voters
+ existing_voters_count = object.data["votersCount"]
voters_count =
- if Map.has_key?(object.data, "votersCount") do
- object.data["votersCount"] + 1
- else
- length(voters)
+ cond do
+ is_integer(existing_voters_count) and new_voter? ->
+ existing_voters_count + 1
+
+ is_integer(existing_voters_count) ->
+ existing_voters_count
+
+ true ->
+ length(voters)
end
data =
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
|> Map.put("votersCount", voters_count)
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
@doc "Updates data field of an object"
def update_data(%Object{data: data} = object, attrs \\ %{}) do
object
|> Object.change(%{data: Map.merge(data || %{}, attrs)})
|> Repo.update()
end
def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
end
def tags(%Object{data: %{"tag" => tags}}) when is_list(tags), do: tags
def tags(_), do: []
def hashtags(%Object{} = object) do
# Note: always using embedded hashtags regardless whether they are migrated to hashtags table
# (embedded hashtags stay in sync anyways, and we avoid extra joins and preload hassle)
embedded_hashtags(object)
end
def embedded_hashtags(%Object{data: data}) do
object_data_hashtags(data)
end
def embedded_hashtags(_), do: []
def object_data_hashtags(%{"tag" => tags}) when is_list(tags) do
tags
|> Enum.filter(fn
%{"type" => "Hashtag"} = data -> Map.has_key?(data, "name")
plain_text when is_bitstring(plain_text) -> true
_ -> false
end)
|> Enum.map(fn
%{"name" => "#" <> hashtag} -> String.downcase(hashtag)
%{"name" => hashtag} -> String.downcase(hashtag)
hashtag when is_bitstring(hashtag) -> String.downcase(hashtag)
end)
|> Enum.uniq()
# Note: "" elements (plain text) might occur in `data.tag` for incoming objects
|> Enum.filter(&(&1 not in [nil, ""]))
end
def object_data_hashtags(_), do: []
def get_emoji_reactions(object) do
reactions = object.data["reactions"]
if is_list(reactions) or is_map(reactions) do
reactions
|> Enum.map(fn
[_emoji, users, _maybe_url] = item when is_list(users) ->
item
[emoji, users] when is_list(users) ->
[emoji, users, nil]
# This case is here to process the Map situation, which will happen
# only with the legacy two-value format.
{emoji, users} when is_list(users) ->
[emoji, users, nil]
_ ->
nil
end)
|> Enum.reject(&is_nil/1)
else
[]
end
end
end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index fca61799b..382cf9db3 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -1,141 +1,141 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature do
@behaviour Pleroma.Signature.API
@behaviour HTTPSignatures.Adapter
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Plug.Conn, only: [put_req_header: 3]
@http_signatures_impl Application.compile_env(
:pleroma,
[__MODULE__, :http_signatures_impl],
HTTPSignatures
)
@known_suffixes ["/publickey", "/main-key"]
def key_id_to_actor_id(key_id) do
uri =
key_id
|> URI.parse()
|> Map.put(:fragment, nil)
|> remove_suffix(@known_suffixes)
maybe_ap_id = URI.to_string(uri)
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
{:ok, ap_id} ->
{:ok, ap_id}
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
{:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end
end
defp remove_suffix(uri, [test | rest]) do
if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
Map.put(uri, :path, String.replace(uri.path, test, ""))
else
remove_suffix(uri, rest)
end
end
defp remove_suffix(uri, []), do: uri
def fetch_public_key(conn) do
with {:ok, actor_id} <- get_actor_id(conn),
{:ok, public_key} <- User.get_or_fetch_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with {:ok, actor_id} <- get_actor_id(conn),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def get_actor_id(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid) do
{:ok, actor_id}
else
e ->
{:error, e}
end
end
def sign(%User{keys: keys} = user, headers) do
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
@spec validate_signature(Plug.Conn.t(), String.t()) :: boolean()
def validate_signature(%Plug.Conn{} = conn, request_target) do
# Newer drafts for HTTP signatures now use @request-target instead of the
# old (request-target). We'll now support both for incoming signatures.
conn =
conn
|> put_req_header("(request-target)", request_target)
|> put_req_header("@request-target", request_target)
- @http_signatures_impl.validate_conn(conn)
+ @http_signatures_impl.validate_conn(conn) == true
end
@spec validate_signature(Plug.Conn.t()) :: boolean()
def validate_signature(%Plug.Conn{} = conn) do
# This (request-target) is non-standard, but many implementations do it
# this way due to a misinterpretation of
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
# "path" was interpreted as not having the query, though later examples
# show that it must be the absolute path + query. This behavior is kept to
# make sure most software (Pleroma itself, Mastodon, and probably others)
# do not break.
request_target = Enum.join([String.downcase(conn.method), conn.request_path], " ")
# This is the proper way to build the @request-target, as expected by
# many HTTP signature libraries, clarified in the following draft:
# https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
# It is the same as before, but containing the query part as well.
proper_target = Enum.join([request_target, "?", conn.query_string], "")
cond do
# Normal, non-standard behavior but expected by Pleroma and more.
validate_signature(conn, request_target) ->
true
# Has query string and the previous one failed: let's try the standard.
conn.query_string != "" ->
validate_signature(conn, proper_target)
# If there's no query string and signature fails, it's rotten.
true ->
false
end
end
end
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 851745714..abc45f62a 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -1,276 +1,322 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
+ alias Pleroma.Instances.Instance
alias Pleroma.Pagination
alias Pleroma.User
import Ecto.Query
@limit 20
def search(query_string, opts \\ []) do
resolve = Keyword.get(opts, :resolve, false)
following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0)
capabilities = Keyword.get(opts, :capabilities, [])
for_user = Keyword.get(opts, :for_user)
query_string = format_query(query_string)
# If this returns anything, it should bounce to the top
maybe_resolved = maybe_resolve(resolve, for_user, query_string)
top_user_ids =
[]
|> maybe_add_resolved(maybe_resolved)
|> maybe_add_ap_id_match(query_string)
|> maybe_add_uri_match(query_string)
results =
query_string
|> search_query(for_user, following, top_user_ids, capabilities)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
results
end
defp maybe_add_resolved(list, {:ok, %User{} = user}) do
[user.id | list]
end
defp maybe_add_resolved(list, _), do: list
defp maybe_add_ap_id_match(list, query) do
if user = User.get_cached_by_ap_id(query) do
[user.id | list]
else
list
end
end
defp maybe_add_uri_match(list, query) do
with {:ok, query} <- UriType.cast(query),
q = from(u in User, where: u.uri == ^query, select: u.id),
users = Pleroma.Repo.all(q) do
users ++ list
else
_ -> list
end
end
defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
with [name, domain] <- String.split(query_string, "@") do
encoded_domain =
domain
|> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
|> String.to_charlist()
|> :idna.encode()
|> to_string()
name <> "@" <> encoded_domain
else
_ -> query_string
end
end
defp search_query(query_string, for_user, following, top_user_ids, capabilities) do
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_invisible_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user)
+ |> filter_unreachable_users()
|> fts_search(query_string)
|> select_top_users(top_user_ids)
|> trigram_rank(query_string)
|> boost_search_rank(for_user, top_user_ids)
|> subquery()
- |> order_by(desc: :search_rank)
+ |> order_by_search_rank(for_user)
|> maybe_restrict_local(for_user)
|> maybe_restrict_accepting_chat_messages(capabilities)
|> filter_deactivated_users()
end
defp select_top_users(query, top_user_ids) do
from(u in query,
or_where: u.id in ^top_user_ids
)
end
defp fts_search(query, query_string) do
query_string = to_tsquery(query_string)
from(
u in query,
where:
fragment(
# The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
"""
(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^query_string
)
)
end
defp to_tsquery(query_string) do
String.trim_trailing(query_string, "@" <> local_domain())
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
end
# Considers nickname match, localized nickname match, name match; preferences nickname match
defp trigram_rank(query, query_string) do
from(
u in query,
select_merge: %{
search_rank:
fragment(
"""
similarity(?, ?) +
similarity(?, regexp_replace(?, '@.+', '')) +
similarity(?, trim(coalesce(?, '')))
""",
^query_string,
u.nickname,
^query_string,
u.nickname,
^query_string,
u.name
)
}
)
end
defp base_query(%User{} = user, true), do: User.get_friends_query(user)
defp base_query(_user, _following), do: User
defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
end
defp filter_internal_users(query) do
from(q in query, where: q.actor_type != "Application")
end
defp filter_deactivated_users(query) do
from(q in query, where: q.is_active == true)
end
defp filter_blocked_user(query, %User{} = blocker) do
query
|> join(:left, [u], b in Pleroma.UserRelationship,
as: :blocks,
on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id
)
|> where([blocks: b], is_nil(b.target_id))
end
defp filter_blocked_user(query, _), do: query
defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks})
when length(domain_blocks) > 0 do
domains = Enum.join(domain_blocks, ",")
from(
q in query,
where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
)
end
defp filter_blocked_domains(query, _), do: query
+ defp filter_unreachable_users(query) do
+ from(u in query,
+ left_join: i in Instance,
+ on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id),
+ where: is_nil(i.unreachable_since)
+ )
+ end
+
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
{:unauthenticated, %User{}} -> User.get_or_fetch(query)
{:unauthenticated, _} -> :noop
{false, _} -> User.get_or_fetch(query)
end
end
defp maybe_resolve(_, _, _), do: :noop
defp maybe_restrict_local(q, user) do
case {limit(), user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp maybe_restrict_accepting_chat_messages(query, capabilities) do
if "accepts_chat_messages" in capabilities do
from(q in query, where: q.accepts_chat_messages == true)
else
query
end
end
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true)
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
defp boost_search_rank(query, %User{} = for_user, top_user_ids) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
+ search_type:
+ fragment(
+ """
+ CASE WHEN (?) THEN 2
+ WHEN (?) THEN 1
+ ELSE 0 END
+ """,
+ u.id in ^top_user_ids,
+ u.id in ^friends_ids or u.id in ^followers_ids
+ ),
search_rank:
fragment(
"""
CASE WHEN (?) THEN (?) * 1.5
WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.1
WHEN (?) THEN 9001
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.id in ^top_user_ids,
u.search_rank
)
}
)
end
defp boost_search_rank(query, _for_user, top_user_ids) do
from(u in subquery(query),
select_merge: %{
+ search_type:
+ fragment(
+ """
+ CASE WHEN (?) THEN 2
+ ELSE 0 END
+ """,
+ u.id in ^top_user_ids
+ ),
search_rank:
fragment(
"""
CASE WHEN (?) THEN 9001
ELSE (?) END
""",
u.id in ^top_user_ids,
u.search_rank
)
}
)
end
+
+ defp order_by_search_rank(query, %User{}) do
+ order_by(
+ query,
+ [u],
+ desc: u.search_type,
+ desc_nulls_last:
+ fragment(
+ "CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END",
+ u.search_type,
+ u.last_status_at,
+ u.last_active_at
+ ),
+ desc: u.search_rank
+ )
+ end
+
+ defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank)
end
diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex
index e7e7e96f9..70e7f6458 100644
--- a/lib/pleroma/web.ex
+++ b/lib/pleroma/web.ex
@@ -1,242 +1,245 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web do
@moduledoc """
A module that keeps using definitions for controllers,
views and so on.
This can be used in your application as:
use Pleroma.Web, :controller
use Pleroma.Web, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below.
"""
alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug
alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.PlugHelper
def controller do
quote do
- use Phoenix.Controller, namespace: Pleroma.Web
+ use Phoenix.Controller, formats: [json: "View", html: "View"]
import Plug.Conn
import Pleroma.Web.Gettext
import Pleroma.Web.TranslationHelpers
alias Pleroma.Web.Router.Helpers, as: Routes
plug(:set_put_layout)
defp set_put_layout(conn, _) do
- put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
+ case Pleroma.Config.get(:app_layout, "app.html") do
+ false -> put_layout(conn, false)
+ layout -> put_layout(conn, {Pleroma.Web.LayoutView, layout})
+ end
end
# Marks plugs intentionally skipped and blocks their execution if present in plugs chain
defp skip_plug(conn, plug_modules) do
plug_modules
|> List.wrap()
|> Enum.reduce(
conn,
fn plug_module, conn ->
try do
plug_module.skip_plug(conn)
rescue
UndefinedFunctionError ->
raise "`#{plug_module}` is not skippable. Append `use Pleroma.Web, :plug` to its code."
end
end
)
end
defp skip_auth(conn, _) do
skip_plug(conn, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug])
end
defp skip_public_check(conn, _) do
skip_plug(conn, EnsurePublicOrAuthenticatedPlug)
end
# Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do
with %{halted: false} = conn <-
maybe_drop_authentication_if_oauth_check_ignored(conn),
%{halted: false} = conn <- maybe_perform_public_or_authenticated_check(conn),
%{halted: false} = conn <- maybe_perform_authenticated_check(conn),
%{halted: false} = conn <- maybe_halt_on_missing_oauth_scopes_check(conn) do
super(conn, params)
end
end
# For non-authenticated API actions, drops auth info if OAuth scopes check was ignored
# (neither performed nor explicitly skipped)
defp maybe_drop_authentication_if_oauth_check_ignored(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
AuthHelper.drop_auth_info(conn)
else
conn
end
end
# Ensures instance is public -or- user is authenticated if such check was scheduled
defp maybe_perform_public_or_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectPublicOrAuthenticatedCheckPlug) do
EnsurePublicOrAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Ensures user is authenticated if such check was scheduled
# Note: runs prior to action even if it was already executed earlier in plug chain
# (since OAuthScopesPlug has option of proceeding unauthenticated)
defp maybe_perform_authenticated_check(conn) do
if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) do
EnsureAuthenticatedPlug.call(conn, %{})
else
conn
end
end
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
defp maybe_halt_on_missing_oauth_scopes_check(conn) do
if PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) and
not PlugHelper.plug_called_or_skipped?(conn, OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
"Security violation: OAuth scopes check was neither handled nor explicitly skipped."
)
|> halt()
else
conn
end
end
end
end
def view do
quote do
use Phoenix.View,
root: "lib/pleroma/web/templates",
namespace: Pleroma.Web
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, view_module: 1]
import Pleroma.Web.ErrorHelpers
import Pleroma.Web.Gettext
alias Pleroma.Web.Router.Helpers, as: Routes
require Logger
@doc "Same as `render/3` but wrapped in a rescue block"
def safe_render(view, template, assigns \\ %{}) do
Phoenix.View.render(view, template, assigns)
rescue
error ->
Logger.error(
"#{__MODULE__} failed to render #{inspect({view, template})}\n" <>
Exception.format(:error, error, __STACKTRACE__)
)
nil
end
@doc """
Same as `render_many/4` but wrapped in rescue block.
"""
def safe_render_many(collection, view, template, assigns \\ %{}) do
Enum.map(collection, fn resource ->
as = Map.get(assigns, :as) || view.__resource__()
assigns = Map.put(assigns, as, resource)
safe_render(view, template, assigns)
end)
|> Enum.filter(& &1)
end
end
end
def router do
quote do
use Phoenix.Router
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
import Phoenix.Channel
import Pleroma.Web.Gettext
end
end
def plug do
quote do
@behaviour Pleroma.Web.Plug
@behaviour Plug
@doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
"""
def skip_plug(conn) do
PlugHelper.append_to_private_list(
conn,
PlugHelper.skipped_plugs_list_id(),
__MODULE__
)
end
@impl Plug
@doc """
Before-plug hook that
* ensures the plug is not skipped
* processes `:if_func` / `:unless_func` functional pre-run conditions
* adds plug to the list of called plugs and calls `perform/2` if checks are passed
Note: multiple invocations of the same plug (with different or same options) are allowed.
"""
def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) ||
(options[:if_func] && !options[:if_func].(conn)) ||
(options[:unless_func] && options[:unless_func].(conn)) do
conn
else
conn =
PlugHelper.append_to_private_list(
conn,
PlugHelper.called_plugs_list_id(),
__MODULE__
)
apply(__MODULE__, :perform, [conn, options])
end
end
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 2bfff6968..3d1875599 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,643 +1,648 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.FederatingPlug
require Logger
action_fallback(:errors)
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
@object_replies_known_param_keys ["page", "min_id", "max_id", "since_id", "limit"]
plug(FederatingPlug when action in @federating_only_actions)
plug(
EnsureAuthenticatedPlug,
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
)
# Note: :following and :followers must be served even without authentication (as via :api)
plug(
EnsureAuthenticatedPlug
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
plug(
Pleroma.Web.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
plug(:log_inbox_metadata when action in [:inbox])
plug(:relay_active? when action in [:relay])
defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn
|> render_error(:not_found, "not found")
|> halt()
end
end
def user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
%{local: false} -> {:error, :not_found}
end
end
def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn
|> maybe_skip_cache(user)
|> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: object)
else
{:visible?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
def object_replies(%{assigns: assigns, query_params: params} = conn, _all_params) do
object_ap_id = conn.path_info |> Enum.reverse() |> tl() |> Enum.reverse()
object_ap_id = Endpoint.url() <> "/" <> Enum.join(object_ap_id, "/")
# Most other API params are converted to atoms by OpenAPISpex 3.x
# and therefore helper functions assume atoms. For consistency,
# also convert our params to atoms here.
params =
params
|> Map.take(@object_replies_known_param_keys)
|> Enum.into(%{}, fn {k, v} -> {String.to_existing_atom(k), v} end)
|> Map.put(:object_ap_id, object_ap_id)
|> Map.put(:order_asc, true)
|> Map.put(:conn, conn)
with %Object{} = object <- Object.get_cached_by_ap_id(object_ap_id),
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn
|> maybe_skip_cache(user)
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object_replies.json", render_params: params)
else
{:visible?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
def track_object_fetch(conn, nil), do: conn
def track_object_fetch(conn, object_id) do
with %{assigns: %{user: %User{id: user_id}}} <- conn do
Delivery.create(object_id, user_id)
end
conn
end
def activity(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:local?, activity.local},
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
conn
|> maybe_skip_cache(user)
|> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: activity)
else
{:visible?, false} -> {:error, :not_found}
{:local?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
object_id = Object.normalize(activity, fetch: false).id
assign(conn, :tracking_fun_data, object_id)
end
defp maybe_set_tracking_data(conn, _activity), do: conn
defp set_cache_ttl_for(conn, %Activity{object: object}) do
set_cache_ttl_for(conn, object)
end
defp set_cache_ttl_for(conn, entity) do
ttl =
case entity do
%Object{data: %{"type" => "Question"}} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
%Object{} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub])
_ ->
nil
end
assign(conn, :cache_ttl, ttl)
end
def maybe_skip_cache(conn, user) do
if user do
conn
|> assign(:skip_cache, true)
else
conn
end
end
# GET /relay/following
def relay_following(conn, _params) do
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else
{:show_follows, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end
end
# GET /relay/followers
def relay_followers(conn, _params) do
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else
{:show_followers, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end
end
def outbox(
%{assigns: %{user: for_user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
# "include_poll_votes" is a hack because postgres generates inefficient
# queries when filtering by 'Answer', poll votes will be hidden by the
# visibility filter in this case anyway
params =
params
|> Map.drop(["nickname", "page"])
|> Map.put("include_poll_votes", true)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities = ActivityPub.fetch_user_activities(user, for_user, params)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities)
})
end
end
def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end
- def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
+ def inbox(
+ %{
+ assigns: %{valid_signature: true, valid_host_header: true}
+ } = conn,
+ %{"nickname" => nickname} = params
+ ) do
with {:recipient_exists, %User{} = recipient} <-
{:recipient_exists, User.get_cached_by_nickname(nickname)},
{:sender_exists, {:ok, %User{} = actor}} <-
{:sender_exists, User.get_or_fetch_by_ap_id(params["actor"])},
{:recipient_active, true} <- {:recipient_active, recipient.is_active},
{:sender_active, true} <- {:sender_active, actor.is_active},
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
else
{:recipient_exists, _} ->
conn
|> put_status(:not_found)
|> json("User does not exist")
{:sender_exists, _} ->
conn
|> put_status(:not_found)
|> json("Sender does not exist")
{:recipient_active, _} ->
conn
|> put_status(:not_found)
|> json("User deactivated")
{:sender_active, _} ->
conn
|> put_status(:not_found)
|> json("Sender deactivated")
_ ->
conn
|> put_status(:bad_request)
|> json("Invalid request.")
end
end
- def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
+ def inbox(%{assigns: %{valid_signature: true, valid_host_header: true}} = conn, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,
params: params,
query_string: conn.query_string
})
json(conn, "ok")
end
# POST /relay/inbox -or- POST /internal/fetch/inbox
def inbox(conn, %{"type" => "Create"} = params) do
if FederatingPlug.federating?() do
post_inbox_relayed_create(conn, params)
else
conn
|> put_status(403)
|> json("Not federating")
end
end
def inbox(conn, _params) do
conn
|> put_status(:bad_request)
|> json("error, missing HTTP Signature")
end
defp post_inbox_relayed_create(conn, params) do
Logger.debug(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
Fetcher.fetch_object_from_id(params["object"]["id"])
json(conn, "ok")
end
defp represent_service_actor(%User{} = user, conn) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
end
defp represent_service_actor(nil, _), do: {:error, :not_found}
def relay(conn, _params) do
Relay.get_actor()
|> represent_service_actor(conn)
end
def internal_fetch(conn, _params) do
InternalFetchActor.get_actor()
|> represent_service_actor(conn)
end
@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
end
def read_inbox(
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
params =
params
|> Map.drop(["nickname", "page"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities)
})
end
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
"nickname" => nickname
}) do
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: as_nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
when is_map(object) do
length =
[object["content"], object["summary"], object["name"]]
|> Enum.filter(&is_binary(&1))
|> Enum.join("")
|> String.length()
limit = Pleroma.Config.get([:instance, :limit])
if length < limit do
object =
object
|> Transmogrifier.strip_internal_fields()
|> Map.put("attributedTo", actor)
|> Map.put("actor", actor)
|> Map.put("id", Utils.generate_object_id())
{:ok, Map.put(activity, "object", object)}
else
{:error,
dgettext(
"errors",
"Character limit (%{limit} characters) exceeded, contains %{length} characters",
limit: limit,
length: length
)}
end
end
defp fix_user_message(
%User{ap_id: actor} = user,
%{"type" => "Delete", "object" => object} = activity
) do
with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
{_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
{:ok, activity}
else
{:normalize, _} ->
{:error, "No such object found"}
{:permission, _} ->
{:forbidden, "You can't delete this object"}
end
end
defp fix_user_message(%User{}, activity) do
{:ok, activity}
end
# We currently lack a Flag ObjectValidator since both CommonAPI and Transmogrifier
# both send it straight to ActivityPub.flag and C2S currently has to go through
# the normal pipeline which requires an ObjectValidator.
# TODO: Add a Flag Activity ObjectValidator
defp check_allowed_action(_, %{"type" => "Flag"}) do
{:error, "Flag activities aren't currently supported in C2S"}
end
# It would respond with 201 and silently fail with:
# Could not decode featured collection at fetch #{user.ap_id} \
# {:error, "Trying to fetch local resource"}
defp check_allowed_action(%{ap_id: ap_id}, %{"type" => "Update", "object" => %{"id" => ap_id}}),
do: {:error, "Updating profile is not currently supported in C2S"}
defp check_allowed_action(_, activity), do: {:ok, activity}
defp validate_visibility(%User{} = user, %{"type" => type, "object" => object} = activity) do
with {_, %Object{} = normalized_object} <-
{:normalize, Object.normalize(object, fetch: false)},
{_, true} <- {:visibility, Visibility.visible_for_user?(normalized_object, user)} do
{:ok, activity}
else
{:normalize, _} ->
if type in ["Create", "Listen"] do
# Creating new object via C2S; user is local and authenticated
# via the :authenticate Plug pipeline.
{:ok, activity}
else
{:error, "No such object found"}
end
{:visibility, _} ->
{:forbidden, "You can't interact with this object"}
end
end
def update_outbox(
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
%{"nickname" => nickname} = params
) do
params =
params
|> Map.drop(["nickname"])
|> Map.put("id", Utils.generate_activity_id())
|> Map.put("actor", actor)
with {:ok, params} <- fix_user_message(user, params),
{:ok, params} <- check_allowed_action(user, params),
{:ok, params} <- validate_visibility(user, params),
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
%Activity{data: activity_data} <- Activity.normalize(activity) do
conn
|> put_status(:created)
|> put_resp_header("location", activity_data["id"])
|> json(activity_data)
else
{:forbidden, message} ->
conn
|> put_status(:forbidden)
|> json(message)
{:error, message} when is_binary(message) ->
conn
|> put_status(:bad_request)
|> json(message)
e ->
Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
conn
|> put_status(:bad_request)
|> json("Bad Request")
end
end
def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
defp errors(conn, _e) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
end
defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do
Logger.metadata(actor: actor, type: type)
conn
end
defp log_inbox_metadata(conn, _), do: conn
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
Logger.debug(inspect(object))
conn
|> put_status(:created)
|> json(object.data)
end
end
def pinned(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("featured.json", %{user: user}))
end
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index c0626ce4d..844ad2c8f 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -1,117 +1,230 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.HTML
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI.Utils
import Ecto.Changeset
@primary_key false
@derive Jason.Encoder
embedded_schema do
quote do
unquote do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
message_fields()
object_fields()
status_object_fields()
end
end
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:source, :map)
end
def cast_and_apply(data) do
data
|> cast_data()
|> apply_action(:insert)
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data
defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
defp fix_url(data), do: data
defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
Map.put(data, "tag", Enum.filter(tag, &is_map/1))
end
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"])
# legacy internal *oma format
defp fix_replies(%{"replies" => replies} = data) when is_list(replies), do: data
defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"first" => %{"orderedItems" => replies}}} = data)
when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"orderedItems" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(data), do: Map.delete(data, "replies")
def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment),
do: Map.put(data, "attachment", [attachment])
def fix_attachments(data), do: data
+ defp remote_mention_resolver(
+ %{"id" => ap_id, "tag" => tags},
+ "@" <> nickname = mention,
+ buffer,
+ opts,
+ acc
+ )
+ when is_binary(ap_id) and is_list(tags) do
+ initial_host =
+ ap_id
+ |> URI.parse()
+ |> Map.get(:host)
+
+ with mention_tag when not is_nil(mention_tag) <-
+ Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
+ href when is_binary(href) <- mention_tag["href"],
+ %User{} = user <- User.get_cached_by_ap_id(href) do
+ link = Pleroma.Formatter.mention_from_user(user, opts)
+ {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
+ else
+ _ -> {buffer, acc}
+ end
+ end
+
+ defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
+
+ defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
+ when is_binary(name) do
+ name == mention || mention == "#{name}@#{initial_host}"
+ end
+
+ defp mention_tag?(_tag, _mention, _initial_host), do: false
+
+ defp scrub_content(%{"content" => content} = object) when is_binary(content) do
+ Map.put(object, "content", HTML.filter_tags(content))
+ end
+
+ defp scrub_content(object), do: object
+
+ defp mfm_parse_limit do
+ min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
+ end
+
+ defp normalize_source(%{"source" => source} = object) when is_binary(source) do
+ object
+ |> Map.put("source", %{"content" => source})
+ |> normalize_source()
+ end
+
+ defp normalize_source(%{"source" => source} = object) when is_map(source) do
+ source =
+ case source["content"] do
+ content when is_binary(content) ->
+ if String.length(content) <= mfm_parse_limit() do
+ source
+ else
+ Map.delete(source, "content")
+ end
+
+ nil ->
+ source
+
+ _ ->
+ Map.delete(source, "content")
+ end
+
+ Map.put(object, "source", source)
+ end
+
+ defp normalize_source(object), do: object
+
+ defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
+ when is_binary(content) do
+ Map.put(object, "content", HTML.filter_tags(content))
+ end
+
+ defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
+
+ defp fix_misskey_content(
+ %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
+ )
+ when is_binary(content) do
+ mention_handler = fn nick, buffer, opts, acc ->
+ remote_mention_resolver(object, nick, buffer, opts, acc)
+ end
+
+ {linked, _mentions, _tags} =
+ Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
+
+ Map.put(object, "content", linked)
+ end
+
+ defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
+ do: scrub_content(object)
+
+ defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
+ object
+ |> Map.put("source", %{
+ "content" => content,
+ "mediaType" => "text/x.misskeymarkdown"
+ })
+ |> Map.delete("_misskey_content")
+ |> fix_misskey_content()
+ end
+
+ defp fix_misskey_content(object), do: object
+
defp fix(data) do
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
|> fix_url()
|> fix_tag()
|> fix_replies()
|> fix_attachments()
+ |> normalize_source()
+ |> fix_misskey_content()
|> CommonFixes.fix_quote_url()
|> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
|> CommonFixes.maybe_add_language()
|> CommonFixes.maybe_add_content_map()
end
def changeset(struct, data) do
data = fix(data)
struct
|> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment)
|> cast_embed(:tag)
end
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Article", "Note", "Page"])
|> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_host_match()
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 22cf0cc05..9b8580200 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -1,71 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
# Activities and Objects, except (Create)ChatMessage
defmacro message_fields do
quote bind_quoted: binding() do
field(:type, :string)
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
end
end
defmacro activity_fields do
quote bind_quoted: binding() do
field(:object, ObjectValidators.ObjectID)
field(:actor, ObjectValidators.ObjectID)
end
end
# All objects except Answer and ChatMessage
defmacro object_fields do
quote bind_quoted: binding() do
field(:content, :string)
field(:contentMap, ObjectValidators.ContentLanguageMap)
+ field(:htmlMfm, :boolean)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)
field(:emoji, ObjectValidators.Emoji, default: %{})
embeds_many(:attachment, AttachmentValidator)
end
end
# Basically objects that aren't ChatMessage and Answer
defmacro status_object_fields do
quote bind_quoted: binding() do
# TODO: Remove actor on objects
field(:actor, ObjectValidators.ObjectID)
field(:attributedTo, ObjectValidators.ObjectID)
embeds_many(:tag, TagValidator)
field(:name, :string)
field(:summary, :string)
field(:context, :string)
field(:sensitive, :boolean, default: false)
field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:quotes_count, :integer, default: 0)
field(:language, ObjectValidators.LanguageCode)
field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUrl, ObjectValidators.ObjectID)
field(:url, ObjectValidators.BareUri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
end
end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 4421da26c..a013dd226 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,1088 +1,1096 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
@behaviour Pleroma.Web.ActivityPub.Transmogrifier.API
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Pleroma.Constants
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object, options \\ []) do
object
|> strip_internal_fields()
|> fix_actor()
|> fix_url()
|> fix_attachments()
|> fix_context()
|> fix_in_reply_to(options)
|> fix_emoji()
|> fix_tag()
|> fix_content_map()
|> fix_addressing()
|> fix_summary()
|> fix_history(&fix_object/1)
end
defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object)
defp maybe_fix_object(object), do: object
defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun)
when is_list(list) do
update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end)
end
defp fix_history(obj, _), do: obj
defp fix_recursive(obj, fun) do
# unlike Erlang, Elixir does not support recursive inline functions
# which would allow us to avoid reconstructing this on every recursion
rec_fun = fn
obj when is_map(obj) -> fix_recursive(obj, fun)
# there may be simple AP IDs in history (or object field)
obj -> obj
end
obj
|> fun.()
|> fix_history(rec_fun)
|> then(fn
%{"object" => object} = doc when is_map(object) ->
update_in(doc["object"], rec_fun)
apdoc ->
apdoc
end)
end
def fix_summary(%{"summary" => nil} = object) do
Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
addrs = map[field]
cond do
is_list(addrs) ->
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
is_binary(addrs) ->
Map.put(map, field, [addrs])
true ->
Map.put(map, field, [])
end
end
@doc """
Bovine compatibility
https://codeberg.org/bovine/bovine/issues/53
"""
def fix_addressing_public(map, field) do
addrs = Map.get(map, field, []) |> List.wrap()
Map.put(
map,
field,
Enum.map(addrs, fn
"Public" -> Pleroma.Constants.as_public()
"as:Public" -> Pleroma.Constants.as_public()
x -> x
end)
)
end
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
do: object
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do
explicit_mentions =
Utils.determine_explicit_mentions(object) ++
[Pleroma.Constants.as_public(), follower_collection]
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
|> Enum.filter(& &1)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
|> Map.put("to", explicit_to)
|> Map.put("cc", final_cc)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
Pleroma.Constants.as_public() in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
Pleroma.Constants.as_public() in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_addressing(object) do
{:ok, %User{follower_address: follower_collection}} =
object
|> Containment.get_actor()
|> User.get_or_fetch_by_ap_id()
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_addressing_public("to")
|> fix_addressing_public("cc")
|> fix_addressing_public("bto")
|> fix_addressing_public("bcc")
|> fix_explicit_addressing(follower_collection)
|> fix_implicit_addressing(follower_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
actor = Containment.get_actor(%{"actor" => actor})
# TODO: Remove actor field for Objects
object
|> Map.put("actor", actor)
|> Map.put("attributedTo", actor)
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.drop(["conversation", "inReplyToAtomUri"])
else
_ ->
object
end
else
object
end
end
def fix_in_reply_to(object, _options), do: object
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
quote_url =
case CommonFixes.fix_quote_url(object) do
%{"quoteUrl" => quote_url} -> quote_url
_ -> nil
end
with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
{:ok, quoted_object} <- get_obj_helper(quote_url, options),
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
Map.put(object, "quoteUrl", quoted_object.data["id"])
else
{:quoting?, _} ->
object
_ ->
object
end
end
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
object
|> Map.put("context", context)
|> Map.drop(["conversation"])
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
url =
cond do
is_list(data["url"]) -> List.first(data["url"])
is_map(data["url"]) -> data["url"]
true -> nil
end
media_type =
cond do
is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
url["mediaType"]
is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
data["mediaType"]
is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
data["mimeType"]
true ->
nil
end
href =
cond do
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
true -> nil
end
if href do
attachment_url =
%{
"href" => href,
"type" => Map.get(url || %{}, "type", "Link")
}
|> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
|> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
%{
"url" => [attachment_url],
"type" => data["type"] || "Document"
}
|> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("name", data["name"])
|> Maps.put_if_present("blurhash", data["blurhash"])
else
nil
end
end)
|> Enum.filter(& &1)
Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
object
|> Map.put("attachment", [attachment])
|> fix_attachments()
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
Map.put(object, "url", url["href"])
end
def fix_url(%{"url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
url_string =
cond do
is_bitstring(first_element) -> first_element
is_map(first_element) -> first_element["href"] || ""
true -> ""
end
Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
|> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
end)
Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn
%{"name" => "#" <> hashtag} -> String.downcase(hashtag)
%{"name" => hashtag} -> String.downcase(hashtag)
end)
Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{} = tag} = object) do
object
|> Map.put("tag", [tag])
|> fix_tag
end
def fix_tag(object), do: object
# prefer content over contentMap
def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
Map.put(object, "content", content)
end
def fix_content_map(object), do: object
defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
options = Keyword.put(options, :fetch, true)
with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do
Map.put(object, "type", "Answer")
else
_ -> object
end
end
defp fix_type(object, _options), do: object
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
+ defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do
+ {:reject, "[Transmogrifier] third-party report: #{account.ap_id}"}
+ end
+
+ defp reject_third_party_report(_, _), do: :ok
+
def handle_incoming(data, options \\ []) do
data
|> fix_recursive(&strip_internal_fields/1)
|> handle_incoming_normalized(options)
end
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
defp handle_incoming_normalized(
%{"type" => "Flag", "object" => objects, "actor" => actor} = data,
_options
) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
+ objects <- List.wrap(objects),
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
+ :ok <- reject_third_party_report(actor, account),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{"cc" => [account.ap_id]}
}
|> ActivityPub.flag()
end
end
# disallow objects with bogus IDs
defp handle_incoming_normalized(%{"id" => nil}, _options), do: :error
defp handle_incoming_normalized(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
defp handle_incoming_normalized(%{"id" => id}, _options)
when is_binary(id) and byte_size(id) < 8,
do: :error
defp handle_incoming_normalized(
%{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
options
) do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
reply_depth = (options[:depth] || 0) + 1
options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
to: data["to"],
object: object,
actor: user,
context: nil,
local: false,
published: data["published"],
additional: Map.take(data, ["cc", "id"])
}
ActivityPub.listen(params)
else
_e -> :error
end
end
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
"laugh" => "😆",
"hmm" => "🤔",
"surprise" => "😮",
"congrats" => "🎉",
"angry" => "💢",
"confused" => "😥",
"rip" => "😇",
"pudding" => "🍮",
"star" => "⭐"
}
# Rewrite misskey likes into EmojiReacts
defp handle_incoming_normalized(
%{
"type" => "Like",
"content" => content
} = data,
options
)
when is_binary(content) do
data
|> Map.put("type", "EmojiReact")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{
"type" => "Like",
"_misskey_reaction" => reaction
} = data,
options
) do
data
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
options
)
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object =
data["object"]
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
|> fix_quote_url_and_maybe_fetch(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
nil <- Activity.get_create_by_object_ap_id(obj_id),
{:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
{:ok, activity}
else
%Activity{} = activity -> {:ok, activity}
e -> e
end
end
defp handle_incoming_normalized(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
e -> {:error, e}
end
end
defp handle_incoming_normalized(
%{"type" => type} = data,
_options
)
when type in ~w{Update Block Follow Accept Reject} do
fixed_obj = maybe_fix_object(data["object"])
data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
defp handle_incoming_normalized(
%{"type" => "Delete"} = data,
_options
) do
with {:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
{:error, {:validate, _}} = e ->
# Check if we have a create activity for this
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <-
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
# We have one, insert a tombstone and retry
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
{:ok, _tombstone} <- Object.create(tombstone_data) do
handle_incoming(data)
else
_ -> e
end
e ->
{:error, e}
end
end
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => %{"type" => type}
} = data,
_options
)
when type in ["Like", "EmojiReact", "Announce", "Block"] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
# For Undos that don't have the complete object attached, try to find it in our database.
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => object
} = activity,
options
)
when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity
|> Map.put("object", data)
|> handle_incoming_normalized(options)
else
_e -> :error
end
end
defp handle_incoming_normalized(
%{
"type" => "Move",
"actor" => origin_actor,
"object" => origin_actor,
"target" => target_actor
},
_options
) do
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
true <- origin_actor in target_user.also_known_as do
ActivityPub.move(origin_user, target_user, false)
else
_e -> :error
end
end
# Rewrite dislikes into the thumbs down emoji
defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do
data
|> Map.put("type", "EmojiReact")
|> Map.put("content", "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data,
options
) do
data
|> put_in(["object", "type"], "EmojiReact")
|> put_in(["object", "content"], "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
options = Keyword.put(options, :fetch, true)
case Object.normalize(id, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end
@spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
ap_id: ap_id
})
when attributed_to == ap_id do
with {:ok, activity} <-
handle_incoming(%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => attributed_to,
"object" => data
}) do
{:ok, Object.normalize(activity, fetch: false)}
else
_ -> get_obj_helper(object_id)
end
end
def get_embedded_obj_helper(object_id, _) do
get_obj_helper(object_id)
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
@doc """
Fedibird compatibility
https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
"""
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
Map.put(object, "quoteUri", quote_url)
end
def set_quote_url(obj), do: obj
@doc """
Inline first page of the `replies` collection,
containing any replies in chronological order.
"""
def set_replies(%{"type" => type} = obj_data)
when type in Pleroma.Constants.status_object_types() do
with obj_ap_id when is_binary(obj_ap_id) <- obj_data["id"],
limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
collection <-
Pleroma.Web.ActivityPub.ObjectView.render("object_replies.json", %{
render_params: %{object_ap_id: obj_data["id"], limit: limit, skip_ap_ctx: true}
}) do
Map.put(obj_data, "replies", collection)
else
0 -> Map.put(obj_data, "replies", obj_data["id"] <> "/replies")
_ -> obj_data
end
end
def set_replies(obj_data), do: obj_data
defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do
Map.merge(obj, %{"votersCount" => length(voters)})
end
defp set_voters_count(obj), do: obj
# Prepares and sanitizes the object for federation.
def prepare_object(object) do
object
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> set_quote_url
|> set_replies
|> set_voters_count
|> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
|> set_type
|> maybe_process_history
end
defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do
processed_history =
Enum.map(
history,
fn
item when is_map(item) -> prepare_object(item)
item -> item
end
)
put_in(object, ["formerRepresentations", "orderedItems"], processed_history)
end
defp maybe_process_history(object) do
object
end
# @doc
# """
# internal -> Mastodon
# """
def prepare_activity(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize(fetch: false)
|> Map.get(:data)
data =
data
|> Map.put("object", prepare_object(object))
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do
data =
data
|> Map.put("object", prepare_object(object))
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.actor_types() do
object =
object
|> maybe_fix_user_object()
|> strip_internal_fields()
data =
data
|> Map.put("object", object)
|> strip_internal_fields()
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do
raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}"
end
def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
|> Object.normalize(fetch: false)
data =
if Visibility.private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
end
data =
data
|> strip_internal_fields
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does.
def prepare_activity(%{"type" => "Accept"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_activity(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_activity(%{"type" => "Flag"} = data) do
with {:ok, stripped_activity} <- Utils.strip_report_status_data(data),
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
{:ok, stripped_activity}
end
end
def prepare_activity(%{"type" => _type} = data) do
data =
data
|> strip_internal_fields
|> maybe_fix_object_url
|> Map.merge(Utils.make_json_ld_header(data))
{:ok, data}
end
def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
with false <- String.starts_with?(object, "http"),
{:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
%{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
relative_object do
Map.put(data, "object", external_url)
else
{:fetch, _} ->
data
_ ->
data
end
end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn
# Expand internal representation tags into AS2 tags.
tag when is_binary(tag) ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
# Do not process tags which are already AS2 tag objects.
tag when is_map(tag) ->
tag
end)
Map.put(object, "tag", tags)
end
# TODO These should be added on our side on insertion, it doesn't make much
# sense to regenerate these all the time
def add_mention_tags(object) do
to = object["to"] || []
cc = object["cc"] || []
mentioned = User.get_users_from_set(to ++ cc, local_only: false)
mentions = Enum.map(mentioned, &build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
def take_emoji_tags(%User{emoji: emoji}) do
emoji
|> Map.to_list()
|> Enum.map(&Emoji.build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out = Enum.map(emoji, &Emoji.build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
end
# TODO: Revisit this
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
def prepare_attachments(object) do
attachments =
object
|> Map.get("attachment", [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
%{
"url" => href,
"mediaType" => media_type,
"name" => data["name"],
"type" => "Document"
}
|> Maps.put_if_present("width", url["width"])
|> Maps.put_if_present("height", url["height"])
|> Maps.put_if_present("blurhash", data["blurhash"])
end)
Map.put(object, "attachment", attachments)
end
def strip_internal_fields(object) do
Map.drop(object, Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
tags = Enum.filter(tags, fn x -> is_map(x) end)
Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 43c0f456d..0af4ceaf5 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,1024 +1,1025 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Logger
require Pleroma.Constants
@supported_object_types [
"Article",
"Note",
"Event",
"Video",
"Page",
"Question",
"Answer",
"Audio",
"Image"
]
@strip_status_report_states ~w(closed resolved)
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
def as_local_public, do: Endpoint.url() <> "/#Public"
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(%{"id" => id} = _), do: id
def get_ap_id(id), do: id
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
@spec determine_explicit_mentions(map()) :: [any]
def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
end)
end
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
object
|> Map.put("tag", [tag])
|> determine_explicit_mentions()
end
def determine_explicit_mentions(_), do: []
@spec label_in_collection?(any(), any()) :: boolean()
defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
defp label_in_collection?(_, _), do: false
@spec label_in_message?(String.t(), map()) :: boolean()
def label_in_message?(label, params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.any?(&label_in_collection?(label, &1))
@spec unaddressed_message?(map()) :: boolean()
def unaddressed_message?(params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.all?(fn
nil -> true
[] -> true
_ -> false
end)
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
do:
label_in_message?(ap_id, params) || unaddressed_message?(params) ||
User.following?(recipient, actor)
defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []
def maybe_splice_recipient(ap_id, params) do
need_splice? =
!label_in_collection?(ap_id, params["to"]) &&
!label_in_collection?(ap_id, params["cc"])
if need_splice? do
cc = [ap_id | extract_list(params["cc"])]
params
|> Map.put("cc", cc)
|> Maps.safe_put_in(["object", "cc"], cc)
else
params
end
end
def make_json_ld_header(data \\ %{}) do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
- "@language" => get_language(data)
+ "@language" => get_language(data),
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
end
defp get_language(%{"language" => language}) when not_empty_string(language) do
language
end
defp get_language(_), do: "und"
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
generate_id("activities")
end
def generate_context_id do
generate_id("contexts")
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Endpoint.url()}/#{type}/#{UUID.generate()}"
end
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
fake_create_activity = %{
"to" => object["to"],
"cc" => object["cc"],
"type" => "Create",
"object" => object
}
get_notified_from_object(fake_create_activity)
end
def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end
def maybe_create_context(context), do: context || generate_id("contexts")
@doc """
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks,
false <- Visibility.local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
:ok
end
def maybe_federate(_), do: :ok
@doc """
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
@spec lazy_put_activity_defaults(map(), boolean) :: map()
def lazy_put_activity_defaults(map, fake? \\ false)
def lazy_put_activity_defaults(map, true) do
map
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
|> lazy_put_object_defaults(true)
end
def lazy_put_activity_defaults(map, _fake?) do
context = maybe_create_context(map["context"])
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> lazy_put_object_defaults(false)
end
# Adds an id and published date if they aren't there.
#
@spec lazy_put_object_defaults(map(), boolean()) :: map()
defp lazy_put_object_defaults(%{"object" => map} = activity, true)
when is_map(map) do
object =
map
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("fake", true)
%{activity | "object" => object}
end
defp lazy_put_object_defaults(%{"object" => map} = activity, _)
when is_map(map) do
object =
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
%{activity | "object" => object}
end
defp lazy_put_object_defaults(activity, _), do: activity
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
map = Map.put(map, "object", object.data["id"])
{:ok, map, object}
end
end
def insert_full_object(map), do: {:ok, map, nil}
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
@spec get_existing_like(String.t(), map()) :: Activity.t() | nil
def get_existing_like(actor, %{data: %{"id" => id}}) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_object_id(id)
|> Activity.Queries.by_type("Like")
|> limit(1)
|> Repo.one()
end
@doc """
Returns like activities targeting an object
"""
def get_object_likes(%{data: %{"id" => id}}) do
id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Like")
|> Repo.all()
end
@spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data(
%User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
activity_id
) do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
%{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_emoji_reaction_data(user, object, emoji, activity_id) do
make_like_data(user, object, activity_id)
|> Map.put("type", "EmojiReact")
|> Map.put("content", emoji)
end
@spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object, count \\ nil) do
length =
count ||
length(element)
data =
Map.merge(
object.data,
%{"#{property}_count" => length, "#{property}s" => element}
)
object
|> Changeset.change(data: data)
|> Object.update_and_set_cache()
end
@spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object
) do
reactions = get_cached_emoji_reactions(object)
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
url = maybe_emoji_url(emoji, activity)
new_reactions =
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil ->
reactions ++ [[emoji, [actor], url]]
index ->
List.update_at(
reactions,
index,
fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
defp maybe_emoji_url(
name,
%Activity{
data: %{
"tag" => [
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
]
}
}
),
do: url
defp maybe_emoji_url(_, _), do: nil
def emoji_count(reactions_list) do
Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
end
def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object
) do
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
reactions = get_cached_emoji_reactions(object)
url = maybe_emoji_url(emoji, activity)
new_reactions =
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil ->
reactions
index ->
List.update_at(
reactions,
index,
fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
)
|> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
def get_cached_emoji_reactions(object) do
Object.get_emoji_reactions(object)
end
@spec add_like_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
[actor | fetch_likes(object)]
|> Enum.uniq()
|> update_likes_in_object(object)
end
@spec remove_like_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
object
|> fetch_likes()
|> List.delete(actor)
|> update_likes_in_object(object)
end
defp update_likes_in_object(likes, object) do
update_element_in_object("like", likes, object)
end
defp fetch_likes(object) do
if is_list(object.data["likes"]) do
object.data["likes"]
else
[]
end
end
#### Follow-related helpers
@doc """
Updates a follow activity's state (for locked accounts).
"""
@spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
"Follow"
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object)
|> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
activity = Activity.get_by_id(activity.id)
{:ok, activity}
end
def update_follow_state(
%Activity{} = activity,
state
) do
new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
@doc """
Makes a follow activity data for the given follower and followed
"""
def make_follow_data(
%User{ap_id: follower_id},
%User{ap_id: followed_id} = _followed,
activity_id
) do
%{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => [Pleroma.Constants.as_public()],
"object" => followed_id,
"state" => "pending"
}
|> Maps.put_if_present("id", activity_id)
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
"Follow"
|> Activity.Queries.by_type()
|> where(actor: ^follower_id)
# this is to use the index
|> Activity.Queries.by_object_id(followed_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def fetch_latest_undo(%User{ap_id: ap_id}) do
"Undo"
|> Activity.Queries.by_type()
|> where(actor: ^ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
emoji = Pleroma.Emoji.maybe_quote(emoji)
"EmojiReact"
|> Activity.Queries.by_type()
|> where(actor: ^ap_id)
|> custom_emoji_discriminator(emoji)
|> Activity.Queries.by_object_id(object_ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
defp custom_emoji_discriminator(query, emoji) do
if String.contains?(emoji, "@") do
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
[name, domain] = String.split(stripped, "@")
domain_pattern = "%/" <> domain <> "/%"
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
query
|> where([activity], fragment("?->>'content' = ?
AND EXISTS (
SELECT FROM jsonb_array_elements(?->'tag') elem
WHERE elem->>'id' ILIKE ?
)", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
else
query
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
end
end
#### Announce-related helpers
@doc """
Returns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
"Announce"
|> Activity.Queries.by_type()
|> where(actor: ^actor)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.one()
end
@doc """
Make announce activity data for the given actor and object
"""
# for relayed messages, we only want to send to subscribers
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
false
) do
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address],
"cc" => [],
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
true
) do
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
data: %{"id" => undone_activity_id, "context" => context},
actor: undone_activity_actor
},
activity_id \\ nil
) do
%{
"type" => "Undo",
"actor" => actor,
"object" => undone_activity_id,
"to" => [follower_address, undone_activity_actor],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> Maps.put_if_present("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object(
%Activity{data: %{"actor" => actor}},
object
) do
unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
announcements = take_announcements(object)
with announcements <- Enum.uniq([actor | announcements]) do
update_element_in_object("announcement", announcements, object)
end
else
{:ok, object}
end
end
def add_announce_to_object(_, object), do: {:ok, object}
@spec remove_announce_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- List.delete(take_announcements(object), actor) do
update_element_in_object("announcement", announcements, object)
end
end
defp take_announcements(%{data: %{"announcements" => announcements}} = _)
when is_list(announcements),
do: announcements
defp take_announcements(_), do: []
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
%{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
|> Maps.put_if_present("id", activity_id)
end
#### Block-related helpers
@spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
"Block"
|> Activity.Queries.by_type()
|> where(actor: ^blocker_id)
# this is to use the index
|> Activity.Queries.by_object_id(blocked_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def make_block_data(blocker, blocked, activity_id) do
%{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
|> Maps.put_if_present("id", activity_id)
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
#### Listen-related helpers
def make_listen_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Listen",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(
%{actor: actor, context: context, content: content} = params,
additional
) do
%{
"type" => "Flag",
"actor" => actor.ap_id,
"content" => content,
"object" => build_flag_object(params),
"context" => context,
"state" => "open",
"rules" => Map.get(params, :rules, nil)
}
|> Map.merge(additional)
end
def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses}) do
[account.ap_id | build_flag_object(%{statuses: statuses})]
end
defp build_flag_object(%{statuses: statuses}) do
Enum.map(statuses || [], &build_flag_object/1)
end
defp build_flag_object(%Activity{} = activity) do
object = Object.normalize(activity, fetch: false)
# Do not allow people to report Creates. Instead, report the Object that is Created.
if activity.data["type"] != "Create" do
build_flag_object_with_actor_and_id(
object,
User.get_by_ap_id(activity.data["actor"]),
activity.data["id"]
)
else
build_flag_object(object)
end
end
defp build_flag_object(%Object{} = object) do
actor = User.get_by_ap_id(object.data["actor"])
build_flag_object_with_actor_and_id(object, actor, object.data["id"])
end
defp build_flag_object(act) when is_map(act) or is_binary(act) do
id =
case act do
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end
case Activity.get_by_ap_id_with_object(id) do
%Activity{object: object} = _ ->
build_flag_object(object)
nil ->
case Object.get_by_ap_id(id) do
%Object{} = object -> build_flag_object(object)
_ -> %{"id" => id, "deleted" => true}
end
end
end
defp build_flag_object(_), do: []
defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do
%{
"type" => "Note",
"id" => id,
"content" => data["content"],
"published" => data["published"],
"actor" =>
AccountView.render(
"show.json",
%{user: actor, skip_visibility_check: true}
)
}
end
#### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
|> Map.put(:type, "Flag")
|> Map.put(:skip_preload, true)
|> Map.put(:preload_report_notes, true)
|> Map.put(:total, true)
|> Map.put(:limit, page_size)
|> Map.put(:offset, (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset)
end
defp maybe_strip_report_status(data, state) do
with true <- Config.get([:instance, :report_strip_status]),
true <- state in @strip_status_report_states,
{:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do
data |> Map.put("object", stripped_activity.data["object"])
else
_ -> data
end
end
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
new_data =
activity.data
|> Map.put("state", state)
|> maybe_strip_report_status(state)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def update_report_state(activity_ids, state) when state in @supported_report_states do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def assign_report_to_account(%Activity{} = activity, nil = _account) do
new_data = Map.delete(activity.data, "assigned_account")
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(%Activity{} = activity, account) do
new_data = Map.put(activity.data, "assigned_account", account)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(activity_ids, account) do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def strip_report_status_data(%Activity{} = activity) do
with {:ok, new_data} <- strip_report_status_data(activity.data) do
{:ok, %{activity | data: new_data}}
end
end
def strip_report_status_data(data) do
[actor | reported_activities] = data["object"]
stripped_activities =
Enum.reduce(reported_activities, [], fn act, acc ->
case ObjectID.cast(act) do
{:ok, act} -> [act | acc]
_ -> acc
end
end)
new_data = put_in(data, ["object"], [actor | stripped_activities])
{:ok, new_data}
end
def get_anonymized_reporter do
with true <- Pleroma.Config.get([:activitypub, :anonymize_reporter]),
nickname when is_binary(nickname) <-
Pleroma.Config.get([:activitypub, :anonymize_reporter_local_nickname]),
%User{ap_id: ap_id, local: true} <- User.get_cached_by_nickname(nickname) do
ap_id
else
_ -> nil
end
end
def maybe_anonymize_reporter(%Activity{data: data} = activity) do
new_data = maybe_anonymize_reporter(data)
%Activity{activity | actor: new_data["actor"], data: new_data}
end
def maybe_anonymize_reporter(activity) do
ap_id = get_anonymized_reporter()
if is_binary(ap_id) do
activity
|> Map.put("actor", ap_id)
else
activity
end
end
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = Pleroma.Constants.as_public()
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Create")
|> Activity.with_preloaded_object()
|> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
@spec maybe_handle_group_posts(Activity.t()) :: :ok
@doc "Automatically repeats posts for local group actor recipients"
def maybe_handle_group_posts(activity) do
poster = User.get_cached_by_ap_id(activity.actor)
User.get_recipients_from_activity(activity)
|> Enum.filter(&match?("Group", &1.actor_type))
|> Enum.reject(&User.blocks?(&1, poster))
|> Enum.each(&Pleroma.Web.CommonAPI.repeat(activity.id, &1))
end
end
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 16489663a..6072fff6b 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -1,372 +1,377 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Language.LanguageDetector
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode,
only: [good_locale_code?: 1]
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
@type t :: %__MODULE__{}
defstruct valid?: true,
errors: [],
user: nil,
params: %{},
status: nil,
summary: nil,
full_payload: nil,
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
quote_post: nil,
visibility: nil,
expires_at: nil,
extra: nil,
emoji: %{},
content_html: nil,
mentions: [],
tags: [],
to: [],
cc: [],
context: nil,
sensitive: false,
language: nil,
object: nil,
preview?: false,
changes: %{}
def new(user, params) do
%__MODULE__{user: user}
|> put_params(params)
end
def create(user, params) do
user
|> new(params)
|> status()
|> summary()
|> with_valid(&attachments/1)
|> full_payload()
|> expires_at()
|> poll()
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&quote_post/1)
|> with_valid(&visibility/1)
|> with_valid(&quoting_visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
|> with_valid(&language/1)
|> sensitive()
|> with_valid(&object/1)
|> preview?()
|> with_valid(&changes/1)
|> validate()
end
def listen(user, params) do
user
|> new(params)
|> visibility()
|> to_and_cc()
|> context()
|> listen_object()
|> with_valid(&changes/1)
|> validate()
end
defp listen_object(%__MODULE__{} = draft) do
object =
draft.params
|> Map.take([:album, :artist, :title, :length])
|> Map.put(:externalLink, Map.get(draft.params, :external_link))
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", "Audio")
|> Map.put("to", draft.to)
|> Map.put("cc", draft.cc)
|> Map.put("actor", draft.user.ap_id)
%{draft | object: object}
end
defp put_params(%__MODULE__{} = draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%{draft | params: params}
end
defp status(%__MODULE__{params: %{status: status}} = draft) do
%{draft | status: String.trim(status)}
end
defp summary(%__MODULE__{params: params} = draft) do
%{draft | summary: Map.get(params, :spoiler_text, "")}
end
defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do
full_payload = String.trim(status <> summary)
case Utils.validate_character_limit(full_payload, draft.attachments) do
:ok -> %{draft | full_payload: full_payload}
{:error, message} -> add_error(draft, message)
end
end
defp attachments(%__MODULE__{params: params} = draft) do
attachments = Utils.attachments_from_ids(params, draft.user)
draft = %{draft | attachments: attachments}
case Utils.validate_attachments_count(attachments) do
:ok -> draft
{:error, message} -> add_error(draft, message)
end
end
defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft
defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft)
when is_binary(id) do
# If a post was deleted all its activities (except the newly added Delete) are purged too,
# thus lookup by Create db ID will yield nil just as if it never existed in the first place.
#
# We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with
# an Announce id it will render it as if it was just the normal referenced post, but
# use the Announce id for replies in the in_reply_to_id key of a POST request to
# /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*.
# TODO: Fix this quirk in FE and remove here and other affected places
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, draft.user),
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
%{draft | in_reply_to: activity}
else
nil ->
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
false ->
add_error(draft, dgettext("errors", "Record not found"))
{:type, type} ->
add_error(
draft,
dgettext("errors", "Can only reply to posts, not %{type} activities",
type: inspect(type)
)
)
end
end
defp in_reply_to(
%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft
) do
%{draft | in_reply_to: in_reply_to}
end
defp in_reply_to(draft), do: draft
defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft)
when not_empty_string(id) do
case Activity.get_by_id_with_object(id) do
%Activity{} = activity ->
%{draft | quote_post: activity}
_ ->
draft
end
end
defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do
quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)})
end
defp quote_post(draft), do: draft
defp in_reply_to_conversation(%__MODULE__{} = draft) do
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
%{draft | in_reply_to_conversation: in_reply_to_conversation}
end
defp visibility(%__MODULE__{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct"))
{visibility, _} ->
%{draft | visibility: visibility}
end
end
defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
true
end
defp can_quote?(draft, object, "private") do
draft.user.ap_id == object.data["actor"]
end
defp can_quote?(_, _, _) do
false
end
defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
draft
else
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
end
end
defp quoting_visibility(draft), do: draft
defp expires_at(%__MODULE__{} = draft) do
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
{:ok, expires_at} -> %{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
end
defp poll(%__MODULE__{} = draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
%{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
{:error, message} ->
add_error(draft, message)
end
end
defp content(%__MODULE__{mentions: mentions} = draft) do
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
mentioned_ap_ids =
Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
mentions =
mentions
|> Kernel.++(mentioned_ap_ids)
|> Utils.get_addressed_users(draft.params[:to])
%{draft | content_html: content_html, mentions: mentions, tags: tags}
end
defp to_and_cc(%__MODULE__{} = draft) do
{to, cc} = Utils.get_to_and_cc(draft)
%{draft | to: to, cc: cc}
end
defp context(%__MODULE__{} = draft) do
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
%{draft | context: context}
end
defp sensitive(%__MODULE__{} = draft) do
sensitive = draft.params[:sensitive]
%{draft | sensitive: sensitive}
end
defp language(%__MODULE__{} = draft) do
language =
with language <- draft.params[:language],
true <- good_locale_code?(language) do
language
else
_ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
end
%{draft | language: language}
end
defp object(%__MODULE__{} = draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
# Sometimes people create posts with subject containing emoji,
# since subjects are usually copied this will result in a broken
# subject when someone replies from an instance that does not have
# the emoji or has it under different shortcode. This is an attempt
# to mitigate this by copying emoji from inReplyTo if they are present
# in the subject.
summary_emoji =
with %Activity{} <- draft.in_reply_to,
%Object{data: %{"tag" => [_ | _] = tag}} <- Object.normalize(draft.in_reply_to) do
Enum.reduce(tag, %{}, fn
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}, acc ->
if String.contains?(draft.summary, name) do
Map.put(acc, name, url)
else
acc
end
_, acc ->
acc
end)
else
_ -> %{}
end
emoji = Map.merge(emoji, summary_emoji)
+ media_type = Utils.get_content_type(draft.params[:content_type])
{:ok, note_data, _meta} = Builder.note(draft)
object =
note_data
|> Map.put("emoji", emoji)
|> Map.put("source", %{
"content" => draft.status,
- "mediaType" => Utils.get_content_type(draft.params[:content_type])
+ "mediaType" => media_type
})
+ |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|> Map.put("generator", draft.params[:generator])
|> Map.put("language", draft.language)
%{draft | object: object}
end
+ defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
+ defp maybe_put(map, _key, _value, _condition), do: map
+
defp preview?(%__MODULE__{} = draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%{draft | preview?: preview?}
end
defp changes(%__MODULE__{} = draft) do
direct? = draft.visibility == "direct"
additional = %{"cc" => draft.cc, "directMessage" => direct?}
additional =
case draft.expires_at do
%DateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
_ -> additional
end
changes =
%{
to: draft.to,
actor: draft.user,
context: draft.context,
object: draft.object,
additional: additional
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
%{draft | changes: changes}
end
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft
defp add_error(%__MODULE__{} = draft, message) do
%{draft | valid?: false, errors: [message | draft.errors]}
end
defp validate(%{valid?: true} = draft), do: {:ok, draft}
defp validate(%{errors: [message | _]}), do: {:error, message}
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 32572a721..26034d685 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,519 +1,537 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
alias Calendar.Strftime
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Plugs.AuthenticationPlug
alias Pleroma.Web.Utils.Params
require Logger
require Pleroma.Constants
def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do
attachments_from_ids_descs(ids, desc, user)
end
def attachments_from_ids(%{media_ids: ids}, user) do
attachments_from_ids_no_descs(ids, user)
end
def attachments_from_ids(_, _), do: []
def attachments_from_ids_no_descs([], _), do: []
def attachments_from_ids_no_descs(ids, user) do
Enum.map(ids, fn media_id ->
case get_attachment(media_id, user) do
%Object{data: data} -> data
_ -> nil
end
end)
|> Enum.reject(&is_nil/1)
end
def attachments_from_ids_descs([], _, _), do: []
def attachments_from_ids_descs(ids, descs_str, user) do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id ->
with %Object{data: data} <- get_attachment(media_id, user) do
Map.put(data, "name", descs[media_id])
end
end)
|> Enum.reject(&is_nil/1)
end
defp get_attachment(media_id, user) do
with %Object{data: data} = object <- Repo.get(Object, media_id),
%{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data,
:ok <- Object.authorize_access(object, user) do
object
else
_ -> nil
end
end
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
def get_to_and_cc(%{visibility: visibility} = draft) do
# If the OP is a DM already, add the implicit actor
mentions =
if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions])
else
draft.mentions
end
get_to_and_cc_for_visibility(
visibility,
draft.user.follower_address,
draft.in_reply_to && draft.in_reply_to.data["actor"],
mentions
)
end
def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do
scope_addr = Pleroma.Constants.as_public()
to =
if parent_actor,
do: Enum.uniq([parent_actor, scope_addr | mentions]),
else: [scope_addr | mentions]
{to, [follower_collection]}
end
def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do
recipients =
if parent_actor,
do: Enum.uniq([parent_actor | mentions]),
else: mentions
to = [
Utils.as_local_public()
| Enum.filter(recipients, fn addr ->
String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/")
end)
]
{to, [follower_collection]}
end
def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do
to =
if parent_actor,
do: Enum.uniq([parent_actor, follower_collection | mentions]),
else: [follower_collection | mentions]
{to, [Pleroma.Constants.as_public()]}
end
def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do
{[follower_collection | mentions], []}
end
def get_to_and_cc_for_visibility("direct", _, _, mentions) do
{mentions, []}
end
def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do
{mentions, []}
end
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
end
def get_addressed_users(mentioned_users, _), do: mentioned_users
def maybe_add_list_data(activity_params, user, {:list, list_id}) do
case Pleroma.List.get(list_id, user) do
%Pleroma.List{} = list ->
activity_params
|> put_in([:additional, "bcc"], [list.ap_id])
|> put_in([:additional, "listMessage"], list.ap_id)
|> put_in([:object, "listMessage"], list.ap_id)
_ ->
activity_params
end
end
def maybe_add_list_data(activity_params, _, _), do: activity_params
def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
when is_binary(expires_in) do
# In some cases mastofe sends out strings instead of integers
data
|> put_in(["poll", "expires_in"], String.to_integer(expires_in))
|> make_poll_data()
end
def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do
limits = Config.get([:instance, :poll_limits])
options = options |> Enum.uniq()
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do
{option_notes, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
note = %{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}
{note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
end)
end_time =
DateTime.utc_now()
|> DateTime.add(expires_in)
|> DateTime.to_iso8601()
key = if Params.truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{:ok, {poll, emoji}}
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
{:error, "Invalid poll"}
end
def make_poll_data(_data) do
{:ok, {%{}, %{}}}
end
defp validate_poll_options_amount(options, %{max_options: max_options}) do
cond do
Enum.count(options) < 2 ->
{:error, "Poll must contain at least 2 options"}
Enum.count(options) > max_options ->
{:error, "Poll can't contain more than #{max_options} options"}
true ->
:ok
end
end
defp validate_poll_options_length(options, %{max_option_chars: max_option_chars}) do
if Enum.any?(options, &(String.length(&1) > max_option_chars)) do
{:error, "Poll options cannot be longer than #{max_option_chars} characters each"}
else
:ok
end
end
defp validate_poll_expiration(expires_in, %{min_expiration: min, max_expiration: max}) do
cond do
expires_in > max -> {:error, "Expiration date is too far in the future"}
expires_in < min -> {:error, "Expiration date is too soon"}
true -> :ok
end
end
def make_content_html(%ActivityDraft{} = draft) do
attachment_links =
draft.params
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> Params.truthy_param?()
content_type = get_content_type(draft.params[:content_type])
options =
if draft.visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
draft.status
|> format_input(content_type, options)
|> maybe_add_attachments(draft.attachments, attachment_links)
end
def get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
def make_context(_, %Participation{} = participation) do
Repo.preload(participation, :conversation).conversation.ap_id
end
def make_context(%Activity{data: %{"context" => context}}, _), do: context
def make_context(_, _), do: Utils.generate_context_id()
def maybe_add_attachments(parsed, _attachments, false = _no_links), do: parsed
def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
text = add_attachments(text, attachments)
{text, mentions, tags}
end
def add_attachments(text, attachments) do
attachment_text = Enum.map(attachments, &build_attachment_link/1)
Enum.join([text | attachment_text], "<br>")
end
defp build_attachment_link(%{"url" => [%{"href" => href} | _]} = attachment) do
name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
end
defp build_attachment_link(_), do: ""
def format_input(text, format, options \\ [])
@doc """
Formatting text to plain text, BBCode, HTML, or Markdown
"""
def format_input(text, "text/plain", options) do
text
|> Formatter.html_escape("text/plain")
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
end
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end
def format_input(text, "text/html", options) do
text
|> Formatter.html_escape("text/html")
|> Formatter.linkify(options)
end
+ def format_input(text, "text/x.misskeymarkdown", options) do
+ text
+ |> Formatter.markdown_to_html(%{breaks: true})
+ |> safe_mfm_to_html()
+ |> Formatter.linkify(options)
+ |> Formatter.html_escape("text/x.misskeymarkdown")
+ end
+
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
|> Formatter.markdown_to_html()
|> Formatter.linkify(options)
|> Formatter.html_escape("text/html")
end
+ defp safe_mfm_to_html(html) do
+ html
+ |> MfmParser.Parser.parse()
+ |> MfmParser.Encoder.to_html()
+ rescue
+ _ -> html
+ catch
+ _, _ -> html
+ end
+
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end
def format_asctime(date) do
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
end
def date_to_asctime(date) when is_binary(date) do
with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
format_asctime(date)
else
_e ->
Logger.warning("Date #{date} in wrong format, must be ISO 8601")
""
end
end
def date_to_asctime(date) do
Logger.warning("Date #{date} in wrong format, must be ISO 8601")
""
end
def to_masto_date(%NaiveDateTime{} = date) do
date
|> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
end
def to_masto_date(date) when is_binary(date) do
with {:ok, date} <- NaiveDateTime.from_iso8601(date) do
to_masto_date(date)
else
_ -> ""
end
end
def to_masto_date(_), do: ""
defp shortname(name) do
with max_length when max_length > 0 <-
Config.get([Pleroma.Upload, :filename_display_max_length], 30),
true <- String.length(name) > max_length do
String.slice(name, 0..max_length) <> "…"
else
_ -> name
end
end
@spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
_ -> {:error, dgettext("errors", "Invalid password.")}
end
end
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity
) do
recipients ++ to
end
def maybe_notify_to_recipients(recipients, _), do: recipients
def maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(activity, fetch: false)
object_data =
cond do
not is_nil(object) ->
object.data
is_map(data["object"]) ->
data["object"]
true ->
%{}
end
tagged_mentions = maybe_extract_mentions(object_data)
recipients ++ tagged_mentions
end
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
user
|> User.get_followers()
|> Enum.map(& &1.ap_id)
|> Enum.concat(recipients)
else
_e -> recipients
end
end
def maybe_notify_followers(recipients, _), do: recipients
def get_notified_subscribers(
%Activity{data: %{"actor" => actor, "type" => "Create"}} = activity
) do
# Do not notify subscribers if author is making a reply
with %Object{data: object} <- Object.normalize(activity, fetch: false),
nil <- object["inReplyTo"],
%User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscriber_users()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)
subscriber_ids
else
_e -> []
end
end
def get_notified_subscribers(_), do: []
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
|> Enum.uniq()
end
def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
else
{:error,
dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)}
end
end
def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
when is_list(status_ids) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end
def get_report_statuses(_, _), do: {:ok, nil}
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
def validate_character_limit(full_payload, _attachments) do
limit = Config.get([:instance, :limit])
length = String.length(full_payload)
if length <= limit do
:ok
else
{:error, dgettext("errors", "The status is over the character limit")}
end
end
def validate_attachments_count([] = _attachments) do
:ok
end
def validate_attachments_count(attachments) do
limit = Config.get([:instance, :max_media_attachments])
count = length(attachments)
if count <= limit do
:ok
else
{:error, dgettext("errors", "Too many attachments")}
end
end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index a5c04a0c4..36e78e1e2 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,207 +1,207 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma
require Pleroma.Constants
alias Pleroma.Config
socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler,
longpoll: false,
websocket: [
path: "/",
compress: false,
- connect_info: [:sec_websocket_protocol],
+ connect_info: [:sec_websocket_headers],
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
fullsweep_after: 20
]
)
socket("/socket", Pleroma.Web.UserSocket,
websocket: [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false,
fullsweep_after: 20
],
longpoll: false
)
socket("/live", Phoenix.LiveView.Socket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Pleroma.Web.Plugs.LoggerMetadataPath)
plug(Pleroma.Web.Plugs.SetLocalePlug)
plug(CORSPlug)
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, max-age=1209600, immutable"
@static_cache_disabled "public, no-cache"
# cache for a day
@favicon_cache_control "public, max=age=86400, immutable"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
# Cache-control headers are duplicated in case we turn off etags in the future
plug(
Pleroma.Web.Plugs.InstanceStatic,
at: "/",
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Pleroma.Web.Plugs.FaviconPlug,
at: "/",
only: ["favicon.png"],
cache_control_for_etags: @favicon_cache_control,
headers: %{
"cache-control" => @favicon_cache_control
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
only: ["index.html"],
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/",
from: :pleroma,
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
plug(Plug.Static,
at: "/pleroma/admin/",
from: {:pleroma, "priv/static/adminfe/"}
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug(Phoenix.CodeReloader)
end
plug(Pleroma.Web.Plugs.TrailingFormatPlug)
plug(Plug.RequestId)
plug(Plug.Logger, log: :debug)
plug(Plug.Parsers,
parsers: [:urlencoded, Pleroma.Web.Multipart, :json],
pass: ["*/*"],
json_decoder: Jason,
# Note: this is compile-time only, won't work for database-config
length: Config.get([:instance, :upload_limit]),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
plug(Plug.MethodOverride)
plug(Plug.Head)
secure_cookies = Config.get([__MODULE__, :secure_cookie_flag])
cookie_name =
if secure_cookies,
do: "__Host-pleroma_key",
else: "pleroma_key"
extra =
Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: cookie_name,
signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
http_only: true,
secure: secure_cookies,
extra: extra
)
plug(Pleroma.Web.Plugs.RemoteIp)
plug(Pleroma.Web.Router)
@doc """
Dynamically loads configuration from the system environment
on startup.
It receives the endpoint configuration from the config files
and must return the updated configuration.
"""
def load_from_system_env(config) do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
end
def websocket_url do
String.replace_leading(url(), "http", "ws")
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 3b4271227..f047804e2 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -1,107 +1,107 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollView do
use Pleroma.Web, :view
alias Pleroma.Web.CommonAPI.Utils
def render("show.json", %{object: object, multiple: multiple, options: options} = params) do
{end_time, expired} = end_time_and_expired(object)
{options, votes_count} = options_and_votes_count(options)
poll = %{
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
expires_at: end_time,
expired: expired,
multiple: multiple,
votes_count: votes_count,
voters_count: voters_count(object),
options: options,
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]),
pleroma: %{
non_anonymous: object.data["nonAnonymous"] || false
}
}
if params[:for] do
# when unauthenticated Mastodon doesn't include `voted` & `own_votes` keys in response
{voted, own_votes} = voted_and_own_votes(params, options)
Map.merge(poll, %{voted: voted, own_votes: own_votes})
else
poll
end
end
def render("show.json", %{object: object} = params) do
case object.data do
%{"anyOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: true, options: options}))
%{"oneOf" => [_ | _] = options} ->
render(__MODULE__, "show.json", Map.merge(params, %{multiple: false, options: options}))
_ ->
nil
end
end
defp end_time_and_expired(object) do
if object.data["closed"] do
end_time = NaiveDateTime.from_iso8601!(object.data["closed"])
expired = NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) == :lt
{Utils.to_masto_date(end_time), expired}
else
{nil, false}
end
end
defp options_and_votes_count(options) do
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
current_count = option["replies"]["totalItems"] || 0
{%{
title: name,
votes_count: current_count
}, current_count + count}
end)
end
+ defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters
+
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
length(voters)
end
- defp voters_count(%{data: %{"votersCount" => voters}}), do: voters
-
defp voters_count(_), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do
if params[:for] do
existing_votes =
Pleroma.Web.ActivityPub.Utils.get_existing_votes(params[:for].ap_id, object)
voted = existing_votes != [] or params[:for].ap_id == object.data["actor"]
own_votes =
if voted do
titles = Enum.map(options, & &1[:title])
Enum.reduce(existing_votes, [], fn vote, acc ->
data = vote |> Map.get(:object) |> Map.get(:data)
index = Enum.find_index(titles, &(&1 == data["name"]))
[index | acc]
end)
else
[]
end
{voted, own_votes}
else
{false, []}
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 3ed1cdd6c..2b698bd5d 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -1,256 +1,264 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Streamer
alias Pleroma.Web.StreamerView
@behaviour Phoenix.Socket.Transport
# Client ping period.
@tick :timer.seconds(30)
@impl Phoenix.Socket.Transport
def child_spec(_opts), do: :ignore
# This only prepares the connection and is not in the process yet
@impl Phoenix.Socket.Transport
def connect(%{params: params} = transport_info) do
with access_token <- find_access_token(transport_info),
{:ok, user, oauth_token} <- authenticate_request(access_token),
{:ok, topic} <-
Streamer.get_topic(params["stream"], user, oauth_token, params) do
topics =
if topic do
[topic]
else
[]
end
state = %{
user: user,
topics: topics,
oauth_token: oauth_token,
count: 0,
timer: nil
}
{:ok, state}
else
{:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(transport_info)}")
{:error, :bad_topic}
{:error, :unauthorized} ->
Logger.debug("#{__MODULE__} authentication error: #{inspect(transport_info)}")
{:error, :unauthorized}
end
end
# All subscriptions/links and messages cannot be created
# until the processed is launched with init/1
@impl Phoenix.Socket.Transport
def init(state) do
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
Process.send_after(self(), :ping, @tick)
{:ok, state}
end
@impl Phoenix.Socket.Transport
def handle_in({text, [opcode: :text]}, state) do
with {:ok, %{} = event} <- Jason.decode(text) do
handle_client_event(event, state)
else
_ ->
Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}")
{:ok, state}
end
end
def handle_in(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
@impl Phoenix.Socket.Transport
def handle_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
message = view.render(template, item, user, topic)
{:push, {:text, message}, %{state | user: user}}
else
{:ok, state}
end
end
def handle_info({:text, text}, state) do
{:push, {:text, text}, state}
end
def handle_info(:ping, state) do
Process.send_after(self(), :ping, @tick)
{:push, {:ping, ""}, state}
end
def handle_info(:close, state) do
{:stop, {:closed, ~c"connection closed by server"}, state}
end
def handle_info(msg, state) do
Logger.debug("#{__MODULE__} received info: #{inspect(msg)}")
{:ok, state}
end
@impl Phoenix.Socket.Transport
def terminate(reason, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)})"
)
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
:ok
end
# Public streams without authentication.
defp authenticate_request(nil) do
{:ok, nil, nil}
end
# Authenticated streams.
defp authenticate_request(access_token) do
with oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user, oauth_token}
else
_ -> {:error, :unauthorized}
end
end
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
with {_, {:ok, topic}} <-
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
{_, false} <- {:subscribed, topic in state.topics} do
Streamer.add_socket(topic, state.oauth_token)
message =
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})
{:reply, :ok, {:text, message}, %{state | topics: [topic | state.topics]}}
else
{:subscribed, true} ->
message =
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})
{:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
message =
StreamerView.render("pleroma_respond.json", %{
type: "subscribe",
result: "error",
error: error
})
{:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params, state) do
with {_, {:ok, topic}} <-
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
{_, true} <- {:subscribed, topic in state.topics} do
Streamer.remove_socket(topic)
message =
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})
{:reply, :ok, {:text, message}, %{state | topics: List.delete(state.topics, topic)}}
else
{:subscribed, false} ->
message =
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})
{:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
message =
StreamerView.render("pleroma_respond.json", %{
type: "unsubscribe",
result: "error",
error: error
})
{:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(
%{"type" => "pleroma:authenticate", "token" => access_token} = _params,
state
) do
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
{:ok, user, oauth_token} <- authenticate_request(access_token) do
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "success"
})
{:reply, :ok, {:text, message}, %{state | user: user, oauth_token: oauth_token}}
else
{:auth, _, _} ->
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :already_authenticated
})
{:reply, :error, {:text, message}, state}
_ ->
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :unauthorized
})
{:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(params, state) do
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
{:ok, state}
end
def handle_error(conn, :unauthorized) do
Plug.Conn.send_resp(conn, 401, "Unauthorized")
end
def handle_error(conn, _reason) do
Plug.Conn.send_resp(conn, 404, "Not Found")
end
- defp find_access_token(%{
- connect_info: %{sec_websocket_protocol: [token]}
- }),
- do: token
+ defp find_access_token(%{connect_info: %{sec_websocket_headers: sec_headers}} = transport_info) do
+ find_sec_websocket_protocol(sec_headers) || find_access_token_from_params(transport_info)
+ end
+
+ defp find_access_token(transport_info), do: find_access_token_from_params(transport_info)
+
+ defp find_sec_websocket_protocol(sec_headers) do
+ Enum.find_value(sec_headers, fn
+ {"sec-websocket-protocol", token} -> token
+ _ -> nil
+ end)
+ end
- defp find_access_token(%{params: %{"access_token" => token}}), do: token
+ defp find_access_token_from_params(%{params: %{"access_token" => token}}), do: token
- defp find_access_token(_), do: nil
+ defp find_access_token_from_params(_), do: nil
end
diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex
new file mode 100644
index 000000000..ab2129f9e
--- /dev/null
+++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex
@@ -0,0 +1,63 @@
+# 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.EnsureHostMatchesPlug do
+ @moduledoc "Ensures Host header matches instance"
+
+ alias Pleroma.Web.Endpoint
+
+ import Plug.Conn
+
+ def init(options), do: options
+
+ @spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
+ def call(%Plug.Conn{assigns: %{valid_signature: true}} = conn, _opts) do
+ # Host header is scheme-less, URI.parse needs the //
+ host_header = get_req_header(conn, "host")
+ host_uri = URI.parse("//#{host_header}")
+ instance_uri = URI.parse(Endpoint.url())
+
+ case host_header do
+ [host] ->
+ cond do
+ host == "" ->
+ resp(conn, 400, "Host header not provided") |> halt()
+
+ true ->
+ if host_matches?(host_uri, instance_uri),
+ do: assign(conn, :valid_host_header, true),
+ else: resp(conn, 400, "Host header does not match this instance") |> halt()
+ end
+
+ [_head | _rest] ->
+ conn
+ |> resp(400, "More than one Host header provided")
+ |> halt()
+
+ [] ->
+ conn
+ |> resp(400, "Host header not provided")
+ |> halt()
+ end
+ end
+
+ # Host header may not be provided, but signature verification failed anyway
+ def call(conn, _opts), do: conn
+
+ defp case_insensitive_compare(checked, authority) do
+ String.downcase(checked) == String.downcase(authority)
+ end
+
+ # Host header did not provide port
+ # Host header is scheme-less, URI.parse does not provide default port
+ defp host_matches?(%URI{host: req_host, port: nil}, %URI{host: instance_host}),
+ do: case_insensitive_compare(req_host, instance_host)
+
+ # Host header provided a port
+ # Any port specified in the Endpoint url configuration is valid here
+ defp host_matches?(%URI{host: req_host, port: port}, %URI{host: instance_host, port: port}),
+ do: case_insensitive_compare(req_host, instance_host)
+
+ defp host_matches?(_, _), do: false
+end
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
index 3a4bffb50..4c2ea099d 100644
--- a/lib/pleroma/web/plugs/remote_ip.ex
+++ b/lib/pleroma/web/plugs/remote_ip.ex
@@ -1,38 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RemoteIp do
@moduledoc """
- This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+ This is a shim to call [`RemoteIp`](https://hex.pm/packages/remote_ip) but with runtime configuration.
"""
alias Pleroma.Config
alias Pleroma.Helpers.InetHelper
import Plug.Conn
@behaviour Plug
def init(_), do: nil
def call(%{remote_ip: original_remote_ip} = conn, _) do
if Config.get([__MODULE__, :enabled]) do
- %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts())
+ new_remote_ip = remote_ip(conn) || original_remote_ip
+
+ conn = %{conn | remote_ip: new_remote_ip}
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
else
conn
end
end
+ defp remote_ip(conn) do
+ opts = remote_ip_opts()
+
+ # Do not use RemoteIp.from/2 here: upstream remote_ip always applies its
+ # built-in reserved ranges. Pleroma keeps :reserved configurable, so reuse
+ # only the header parsing and apply Pleroma's own block classification.
+ conn.req_headers
+ |> RemoteIp.Headers.take(opts[:headers])
+ |> RemoteIp.Headers.parse()
+ |> Enum.reverse()
+ |> Enum.find(&client?(&1, opts))
+ end
+
defp remote_ip_opts do
- headers = Config.get([__MODULE__, :headers], []) |> MapSet.new()
reserved = Config.get([__MODULE__, :reserved], [])
proxies =
Config.get([__MODULE__, :proxies], [])
|> Enum.concat(reserved)
|> Enum.map(&InetHelper.parse_cidr/1)
- {headers, proxies}
+ clients =
+ Config.get([__MODULE__, :clients], [])
+ |> Enum.map(&InetHelper.parse_cidr/1)
+
+ [
+ headers: Config.get([__MODULE__, :headers], []),
+ clients: clients,
+ proxies: proxies
+ ]
+ end
+
+ defp client?(ip, opts) do
+ client_ip?(ip, opts[:clients]) || !proxy_ip?(ip, opts[:proxies])
+ end
+
+ defp client_ip?(ip, clients) do
+ Enum.any?(clients, &InetCidr.contains?(&1, ip))
+ end
+
+ defp proxy_ip?(ip, proxies) do
+ Enum.any?(proxies, &InetCidr.contains?(&1, ip))
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 8fe0444db..4b1c4140b 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,1128 +1,1129 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
import Oban.Web.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
# Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
# Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
pipeline :app_api do
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :admin_api do
plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :require_admin do
plug(Pleroma.Web.Plugs.UserIsAdminPlug)
end
pipeline :require_privileged_role_users_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_delete)
end
pipeline :require_privileged_role_users_manage_credentials do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_credentials)
end
pipeline :require_privileged_role_messages_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_read)
end
pipeline :require_privileged_role_users_manage_tags do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_tags)
end
pipeline :require_privileged_role_users_manage_activation_state do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state)
end
pipeline :require_privileged_role_users_manage_invites do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_invites)
end
pipeline :require_privileged_role_reports_manage_reports do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :reports_manage_reports)
end
pipeline :require_privileged_role_users_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_read)
end
pipeline :require_privileged_role_messages_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_delete)
end
pipeline :require_privileged_role_emoji_manage_emoji do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :emoji_manage_emoji)
end
pipeline :require_privileged_role_instances_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :instances_delete)
end
pipeline :require_privileged_role_moderation_log_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :moderation_log_read)
end
pipeline :require_privileged_role_statistics_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :statistics_read)
end
pipeline :require_privileged_role_announcements_manage_announcements do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :announcements_manage_announcements)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :well_known do
plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
+ plug(Pleroma.Web.Plugs.EnsureHostMatchesPlug)
end
pipeline :inbox_guard do
plug(Pleroma.Web.Plugs.InboxGuardPlug)
end
pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
scope "/api/v1/pleroma", Pleroma.Web.OAuth do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:pleroma_api)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
get("/federation_status", InstancesController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_api)
post("/remote_interaction", RemoteInteractionController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
get("/rules", RuleController, :index)
post("/rules", RuleController, :create)
patch("/rules/:id", RuleController, :update)
delete("/rules/:id", RuleController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_announcements_manage_announcements)
get("/announcements", AnnouncementController, :index)
post("/announcements", AnnouncementController, :create)
get("/announcements/:id", AnnouncementController, :show)
patch("/announcements/:id", AnnouncementController, :change)
delete("/announcements/:id", AnnouncementController, :delete)
get("/webhooks", WebhookController, :index)
get("/webhooks/:id", WebhookController, :show)
post("/webhooks", WebhookController, :create)
patch("/webhooks/:id", WebhookController, :update)
delete("/webhooks/:id", WebhookController, :delete)
post("/webhooks/:id/enable", WebhookController, :enable)
post("/webhooks/:id/disable", WebhookController, :disable)
post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_delete)
delete("/users", UserController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_credentials)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_read)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/statuses", StatusController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
get("/instances/:instance/statuses", InstanceController, :list_statuses)
get("/statuses/:id", StatusController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_tags)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_activation_state)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_invites)
patch("/users/approve", UserController, :approve)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_reports_manage_reports)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/assign_account", ReportController, :assign_account)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_read)
get("/users", UserController, :index)
get("/users/:nickname", UserController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_delete)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_instances_delete)
delete("/instances/:instance", InstanceController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_moderation_log_read)
get("/moderation_log", AdminAPIController, :list_log)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_statistics_read)
get("/stats", AdminAPIController, :stats)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
delete("/", EmojiPackController, :delete)
end
scope "/pack" do
pipe_through(:api)
get("/", EmojiPackController, :show)
end
# Modifying packs
scope "/packs" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
post("/download_zip", EmojiPackController, :download_zip)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
get("/", EmojiPackController, :index)
get("/archive", EmojiPackController, :archive)
end
end
scope "/", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_html)
post("/main/ostatus", RemoteInteractionController, :remote_subscribe)
get("/main/ostatus", RemoteInteractionController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteInteractionController, :follow)
post("/ostatus_subscribe", RemoteInteractionController, :do_follow)
get("/authorize_interaction", RemoteInteractionController, :authorize_interaction)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notification_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
put("/aliases", UtilController, :add_alias)
get("/aliases", UtilController, :list_aliases)
delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/mutes_import", UserImportController, :mutes)
post("/blocks_import", UserImportController, :blocks)
post("/follow_import", UserImportController, :follow)
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
end
scope "/oauth", Pleroma.Web.OAuth do
# Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
get("/registration_details", OAuthController, :registration_details)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
get(
"/preferred_frontend/available",
FrontendSettingsController,
:available_frontends
)
put(
"/preferred_frontend",
FrontendSettingsController,
:update_preferred_frontend
)
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
post("/chats/by-account-id/:id", ChatController, :create)
get("/chats", ChatController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
post("/chats/:id/messages", ChatController, :post_chat_message)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
post("/chats/:id/pin", ChatController, :pin)
post("/chats/:id/unpin", ChatController, :unpin)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :create)
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
get("/bookmark_folders", BookmarkFolderController, :index)
post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
get("/outgoing_follow_requests", FollowRequestController, :outgoing)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
get("/statuses/:id/quotes", StatusController, :quotes)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
get("/birthdays", AccountController, :birthdays)
end
scope [] do
pipe_through(:authenticated_api)
get("/settings/:app", SettingsController, :show)
patch("/settings/:app", SettingsController, :update)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
scope "/api/v1/pleroma", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/:id/endorsements", AccountController, :endorsements)
end
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/chats", ChatController, :index2)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists)
get("/endorsements", AccountController, :own_endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
post("/follows", AccountController, :follow_by_uri)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse)
post("/accounts/:id/endorse", AccountController, :endorse)
post("/accounts/:id/unendorse", AccountController, :unendorse)
post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
delete("/conversations/:id", ConversationController, :delete)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
get("/media/:id", MediaController, :show)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
# Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/statuses/:id/translate", StatusController, :translate)
get("/statuses/:id/quotes", StatusController, :quotes)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
get("/tags/:id", TagController, :show)
post("/tags/:id/follow", TagController, :follow)
post("/tags/:id/unfollow", TagController, :unfollow)
get("/followed_tags", TagController, :show_followed)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:app_api)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/search", SearchController, :search)
get("/accounts/lookup", AccountController, :lookup)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
get("/instance/rules", InstanceController, :rules)
get("/instance/domain_blocks", InstanceController, :domain_blocks)
get("/instance/translation_languages", InstanceController, :translation_languages)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
get("/statuses/:id/source", StatusController, :show_source)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
get("/instance", InstanceController, :show2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/pleroma/frontend_configurations", PleromaAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
OAuth.TokenController,
:confirm_email,
as: :confirm_email
)
end
scope "/api" do
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_pleroma_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", OAuth.TokenController, :oauth_tokens)
delete("/oauth_tokens/:id", OAuth.TokenController, :revoke_token)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, :static_fe])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through([:accepts_html_xml])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
get("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :switch)
post("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :do_switch)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
plug(Pleroma.Web.Plugs.APClientApiEnabledPlug)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
# AP interactions used by both S2S and C2S
pipeline :activitypub_server_or_client do
plug(:ap_service_actor)
plug(:fetch_session)
plug(:authenticate)
plug(Pleroma.Web.Plugs.APClientApiEnabledPlug, allow_server: true)
plug(:after_auth)
plug(:http_signature)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_server_or_client])
get("/users/:nickname/outbox", ActivityPubController, :outbox)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
get("/objects/:uuid/replies", ActivityPubController, :object_replies)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub, :inbox_guard])
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
end
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
post("/auth/password", OAuth.PasswordController, :request)
get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web do
get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/pleroma/live_dashboard", additional_pages: [oban: Oban.LiveDashboard])
oban_dashboard("/pleroma/oban")
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
:do_oauth_check,
:fallback_oauth_check,
:skip_oauth_check,
:fallback_oauth_skip_publicity_check,
:skip_oauth_skip_publicity_check,
:missing_oauth_check_definition
]
scope "/test/api", Pleroma.Tests do
pipe_through(:api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, [])
get("/api/*path", RedirectController, :api_not_implemented)
get("/phoenix/live_dashboard/*path", RedirectController, :live_dashboard)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
# /pleroma/{phoenix,oban}/* need to get filtered out from api routes for frontend configuration
# to not drop admin overrides for /pleroma/admin.
@non_api_routes ["/pleroma/live_dashboard", "/pleroma/oban"]
def get_api_routes do
Phoenix.Router.routes(__MODULE__)
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.reject(fn r -> String.starts_with?(r.path, @non_api_routes) end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end
diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex
index 2c4c097dd..28958faff 100644
--- a/lib/pleroma/workers/signature_retry_worker.ex
+++ b/lib/pleroma/workers/signature_retry_worker.ex
@@ -1,254 +1,271 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SignatureRetryWorker do
alias Pleroma.Instances
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator
+ alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
require Logger
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true
def perform(%Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => method,
"params" => params,
"req_headers" => req_headers,
"request_path" => request_path,
"query_string" => query_string
}
})
when is_binary(method) and is_map(params) and is_list(req_headers) and
is_binary(request_path) and is_binary(query_string) do
case normalize_req_headers(req_headers) do
{:ok, req_headers} ->
conn_data = %Plug.Conn{
assigns: %{valid_signature: true},
method: method,
params: params,
req_headers: req_headers,
request_path: request_path,
query_string: query_string
}
signature_actor_result = signature_actor_id(conn_data)
with actor_id = Utils.get_ap_id(params["actor"]),
{:signature_actor, {:ok, signature_actor_id}} <-
{:signature_actor, signature_actor_result},
{:same_actor, true} <- {:same_actor, signature_actor_id == actor_id},
{:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id),
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
{:signature, true} <- {:signature, validate_signature(conn_data)},
{:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
+ {:host_header, true} <- {:host_header, validate_host_header(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
end
{:ok, res}
else
e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result))
end
e ->
process_errors(e, retry_log_context(params, request_path, nil))
end
end
def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do
process_errors(
:missing_signature_retry_metadata,
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
)
end
def perform(%Job{args: args}) when is_map(args) do
process_errors(
:missing_signature_retry_metadata,
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
)
end
def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata)
@impl true
def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
def timeout(_job), do: :timer.seconds(5)
defp normalize_req_headers(req_headers) do
req_headers
|> Enum.reduce_while({:ok, []}, fn
{key, value}, {:ok, acc} when is_binary(key) and is_binary(value) ->
{:cont, {:ok, [{key, value} | acc]}}
[key, value], {:ok, acc} when is_binary(key) and is_binary(value) ->
{:cont, {:ok, [{key, value} | acc]}}
_, _ ->
{:halt, {:error, :invalid_signature_retry_metadata}}
end)
|> case do
{:ok, headers} -> {:ok, Enum.reverse(headers)}
error -> error
end
end
+ defp validate_host_header(conn_data) do
+ case EnsureHostMatchesPlug.call(conn_data, []) do
+ %Plug.Conn{assigns: %{valid_signature: true, valid_host_header: true}} ->
+ true
+
+ _ ->
+ false
+ end
+ end
+
defp validate_same_actor(conn_data) do
case MappedSignatureToIdentityPlug.call(conn_data, []) do
%Plug.Conn{assigns: %{valid_signature: true}} ->
true
_ ->
false
end
end
defp validate_signature(conn_data) do
Signature.validate_signature(conn_data)
rescue
_ -> false
catch
_, _ -> false
end
defp signature_actor_id(conn_data) do
Signature.get_actor_id(conn_data)
rescue
_ -> {:error, :invalid_signature}
catch
_, _ -> {:error, :invalid_signature}
end
defp process_errors(errors, context \\ %{})
defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context)
defp process_errors(errors, context) do
result =
case errors do
# User fetch failures
{:error, :not_found} = reason ->
{:cancel, reason}
{:error, :forbidden} = reason ->
{:cancel, reason}
# Inactive user
{:error, {:user_active, false} = reason} ->
{:cancel, reason}
# Validator will error and return a changeset error
# e.g., duplicate activities or if the object was deleted
{:error, {:validate, {:error, _changeset} = reason}} ->
{:cancel, reason}
# Duplicate detection during Normalization
{:error, :already_present} ->
{:cancel, :already_present}
# MRFs will return a reject
{:error, {:reject, _} = reason} ->
{:cancel, reason}
# HTTP Sigs
{:signature_actor, {:error, _}} ->
{:cancel, :invalid_signature}
{:signature, false} ->
{:cancel, :invalid_signature}
{:same_actor, false} ->
{:cancel, :actor_signature_mismatch}
+ # Host header from request not for us
+ {:host_header, false} ->
+ {:cancel, :host_header_mismatch}
+
# Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} ->
{:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} ->
{:cancel, reason}
# Fail closed if the retry cannot reconstruct the original request.
:missing_signature_retry_metadata ->
{:cancel, :missing_signature_retry_metadata}
{:error, :invalid_signature_retry_metadata} ->
{:cancel, :invalid_signature_retry_metadata}
# Catchall
{:error, _} = e ->
e
e ->
{:error, e}
end
log_signature_retry_rejection(result, context)
result
end
defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do
signature_actor =
case signature_actor_result do
{:ok, actor} when is_binary(actor) -> actor
actor when is_binary(actor) -> actor
_ -> nil
end
%{
activity_id: params["id"],
payload_actor: Utils.get_ap_id(params["actor"]),
request_path: request_path,
signature_actor: signature_actor,
type: params["type"]
}
end
defp retry_log_context(_params, request_path, signature_actor_result) do
signature_actor =
case signature_actor_result do
{:ok, actor} when is_binary(actor) -> actor
actor when is_binary(actor) -> actor
_ -> nil
end
%{
activity_id: nil,
payload_actor: nil,
request_path: request_path,
signature_actor: signature_actor,
type: nil
}
end
defp log_signature_retry_rejection({:cancel, reason}, context)
when reason in [
:actor_signature_mismatch,
+ :host_header_mismatch,
:invalid_signature,
:invalid_signature_retry_metadata,
:missing_signature_retry_metadata,
:origin_containment_failed
] do
Logger.warning(
"Failed-signature inbox retry rejected " <>
"reason=#{inspect(reason)} " <>
"payload_actor=#{inspect(context[:payload_actor])} " <>
"signature_actor=#{inspect(context[:signature_actor])} " <>
"activity_id=#{inspect(context[:activity_id])} " <>
"type=#{inspect(context[:type])} " <>
"request_path=#{inspect(context[:request_path])}"
)
end
defp log_signature_retry_rejection(_result, _context), do: :ok
end
diff --git a/mix.exs b/mix.exs
index bb7bfb9da..3d525b321 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,369 +1,364 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
version: version("2.10.2"),
elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors(), prune_code_paths: false],
xref: [exclude: [:eldap]],
dialyzer: [plt_add_apps: [:mix, :eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: :covertool, summary: true],
# Docs
name: "Pleroma",
homepage_url: "https://pleroma.social/",
source_url: "https://git.pleroma.social/pleroma/pleroma",
docs: [
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
logo: "priv/static/images/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
Configuration: Path.wildcard("docs/config/*.md"),
Administration: Path.wildcard("docs/admin/*.md"),
"Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md")
],
main: "readme",
output: "priv/static/doc"
],
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &copy_files/1, &copy_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]
]
]
end
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
end
def copy_nginx_config(%{path: target_path} = release) do
File.cp!(
"./installation/pleroma.nginx",
Path.join([target_path, "installation", "pleroma.nginx"])
)
release
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Pleroma.Application, []},
extra_applications:
[
:logger,
:runtime_tools,
:comeonin,
:fast_sanitize,
:os_mon,
:ssl,
:eldap
] ++ logger_application(),
included_applications: [:ex_syslogger]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks", "priv/scrubbers"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors, do: System.get_env("CI") == "true"
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
dependency
else
[strategy] -> "ueberauth_#{strategy}"
end
end)
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
end
defp logger_application do
if Version.match?(System.version(), "<1.15.0-rc.0") do
[]
else
[:logger_backends]
end
end
defp logger_deps do
if Version.match?(System.version(), "<1.15.0-rc.0") do
[]
else
[{:logger_backends, "~> 1.0.0"}]
end
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
- {:phoenix,
- git: "https://github.com/feld/phoenix", branch: "v1.7.14-websocket-headers", override: true},
+ {:phoenix, "~> 1.8.0"},
{:phoenix_ecto, "~> 4.6"},
{:ecto_sql, "~> 3.13"},
{:ecto_enum, "~> 1.4"},
{:postgrex, ">= 0.21.1"},
{:phoenix_html, "~> 3.3"},
{:phoenix_live_view, "~> 1.1.19"},
{:phoenix_live_dashboard, "~> 0.8.7"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.3"},
{:tzdata, "~> 1.0.5"},
{:plug_cowboy, "~> 2.7"},
{:oban, "~> 2.19.4"},
- {:oban_plugins_lazarus,
- git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git",
- ref: "e49fc355baaf0e435208bf5f534d31e26e897711"},
+ {:oban_plugins_lazarus, "~> 0.1.1"},
{:oban_web, "~> 2.11"},
{:gettext, "~> 0.24"},
{:bcrypt_elixir, "~> 2.3"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.6"},
{:tesla, "~> 1.15"},
{:castore, "~> 1.0"},
{:cowlib, "~> 2.15"},
{:gun, "~> 2.2"},
{:finch, "~> 0.20"},
{:jason, "~> 1.4"},
{:mogrify, "~> 0.9.3", override: true},
{:ex_aws, "~> 2.1.9"},
{:ex_aws_s3, "~> 2.5"},
{:sweet_xml, "~> 0.7.5"},
{:earmark, "1.4.46"},
{:bbcode_pleroma, "~> 0.2.0"},
+ {:pleroma_mfm_parser, "~> 0.2.1"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
{:swoosh, "~> 1.16.12"},
{:phoenix_swoosh, "~> 1.2"},
{:gen_smtp, "~> 0.15"},
{:mua, "~> 0.2.4"},
{:mail, "~> 0.3.1"},
{:ex_syslogger, "~> 1.5"},
{:floki, "~> 0.38"},
{:timex, "~> 3.7"},
{:ueberauth, "~> 0.10"},
{:linkify, "~> 0.5.3"},
- {:http_signatures, "~> 0.1.2"},
+ {:http_signatures, "~> 0.1.3"},
{:telemetry, "~> 1.0.0", override: true},
{:poolboy, "~> 1.5"},
{:prom_ex, "~> 1.9"},
{:recon, "~> 2.5"},
{:joken, "~> 2.6"},
{:pot, "~> 1.0"},
{:ex_const, "~> 0.3"},
{:plug_static_index_html, "~> 1.0.0"},
{:flake_id, "~> 0.1.0"},
{:concurrent_limiter, "~> 0.1.1"},
- {:remote_ip,
- git: "https://git.pleroma.social/pleroma/remote_ip.git",
- ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
- {:captcha,
- git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
- ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"},
+ {:remote_ip, "~> 1.2.0"},
+ {:inet_cidr, "~> 1.0"},
+ {:captcha, "~> 1.0.3", hex: :pleroma_captcha},
{:restarter, path: "./restarter"},
- {:majic, "~> 1.1"},
+ {:majic, "~> 1.2"},
{:open_api_spex, "~> 3.22"},
{:ecto_psql_extras, "~> 0.8"},
{:vix, "~> 0.36"},
{:elixir_make, "~> 0.7.8", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
{:exile, "~> 0.10.0"},
{:bandit, "~> 1.10"},
{:websock_adapter, "~> 0.5.8"},
{:oban_live_dashboard, "~> 0.1.1"},
{:multipart, "~> 0.4.0", optional: true},
{:argon2_elixir, "~> 4.1"},
## dev & test
{:phoenix_live_reload, "~> 1.3.3", only: :dev},
{:poison, "~> 3.1", only: :test},
{:ex_doc, "~> 0.38", only: :dev, runtime: false},
{:ex_machina, "~> 2.8", only: :test},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.9", only: :test},
{:covertool, "~> 2.0", only: :test},
{:hackney, "~> 1.20.1", override: true},
{:mox, "~> 1.2", only: :test},
{:websockex, "~> 0.4.3", only: :test},
{:benchee, "~> 1.4", only: :benchmark},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
] ++ oauth_deps() ++ logger_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.migrate": ["pleroma.ecto.migrate"],
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "pleroma.ecto.migrate --quiet", "test --warnings-as-errors"],
docs: ["pleroma.docs", "docs"],
analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"],
copyright: &add_copyright/1,
"copyright.bump": &bump_copyright/1
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * branch name
# * build metadata:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
git_available? = match?({_output, 0}, System.cmd("sh", ["-c", "command -v git"]))
dotgit_present? = File.exists?(".git")
git_pre_release =
if git_available? and dotgit_present? do
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
{describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
{commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
# Pre-release version, denoted from patch version with a hyphen
cond do
tag_err == 0 and describe_err == 0 ->
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
commit_hash_err == 0 ->
"0-g" <> String.trim(commit_hash)
true ->
nil
end
end
# Branch name as pre-release version component, denoted with a dot
branch_name =
with true <- git_available?,
true <- dotgit_present?,
{branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()
|> String.replace(identifier_filter, "-")
branch_name
else
_ -> ""
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
env_override = System.get_env("PLEROMA_BUILD_ENV")
env_name =
case env_override do
nil -> env_name
env_override when env_override in ["", "prod"] -> nil
env_override -> env_override
end
# Pre-release version, denoted by appending a hyphen
# and a series of dot separated identifiers
pre_release =
[git_pre_release, branch_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "-" <> String.replace(string, identifier_filter, "-")
end).()
# Build metadata, denoted with a plus sign
build_metadata =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "+" <> String.replace(string, identifier_filter, "-")
end).()
[version, pre_release, build_metadata]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
defp add_copyright(_) do
year = NaiveDateTime.utc_now().year
template = ~s[\
# Pleroma: A lightweight social networking server
# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
] |> String.replace("\n", "\\n")
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\) -exec "
grep = "grep -L '# Copyright © [0-9\-]* Pleroma' {} \\;"
xargs = "xargs -n1 sed -i'' '1s;^;#{template};'"
:os.cmd(String.to_charlist("#{find}#{grep} | #{xargs}"))
end
defp bump_copyright(_) do
year = NaiveDateTime.utc_now().year
find = "find lib test priv -type f \\( -name '*.ex' -or -name '*.exs' \\)"
xargs =
"xargs sed -i'' 's;# Copyright © [0-9\-]* Pleroma.*$;# Copyright © 2017-#{year} Pleroma Authors <https://pleroma.social/>;'"
:os.cmd(String.to_charlist("#{find} | #{xargs}"))
end
end
diff --git a/mix.lock b/mix.lock
index dd5f65f11..cde864084 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,161 +1,162 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
"benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"},
"blurhash": {:hex, :rinpatch_blurhash, "0.1.0", "01a888b0f5f1f382ab52e4396f01831cbe8486ea5828604c90f4dac533d39a4b", [:mix], [{:mogrify, "~> 0.8.0", [hex: :mogrify, repo: "hexpm", optional: true]}], "hexpm", "19911a5dcbb0acb9710169a72f702bce6cb048822b12de566ccd82b2cc42b907"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
- "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]},
+ "captcha": {:hex, :pleroma_captcha, "1.0.3", "6cc1440e91a092653fe909f028cc4c5d83ea858b1439e3b9a85e446382e2b9a3", [:make, :mix], [], "hexpm", "477f62c1a845a9458c546658223295f958f98136acef89c05beb278b1b6f4a14"},
"castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"},
"covertool": {:hex, :covertool, "2.0.7", "398be995c4cf1a2861174389b3577ca97beee43b60c8f1afcf510f1b07d32408", [:rebar3], [], "hexpm", "46158ed6e1a0df7c0a912e314c7b8e053bd74daa5fc6b790614922a155b5720c"},
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]},
"ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.8", "aa02529c97f69aed5722899f5dc6360128735a92dd169f23c5d50b1f7fdede08", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "04c63d92b141723ad6fed2e60a4b461ca00b3594d16df47bbc48f1f4534f2c49"},
"ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"},
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"exile": {:hex, :exile, "0.10.0", "b69e2d27a9af670b0f0a0898addca0eda78f6f5ba95ccfbc9bc6ccdd04925436", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c62ee8fee565b5ac4a898d0dcd58d2b04fb5eec1655af1ddcc9eb582c6732c33"},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
"fast_html": {:hex, :fast_html, "2.5.0", "23578c1c1fa03db6a3786d78218b2d944df34b0fc3729e72de912f390913c80f", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "69eb46ed98a5d9cca1ccd4a5ac94ce5dd626fc29513fbaa0a16cd8b2da67ae3e"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
- "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
+ "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
- "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
+ "inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
- "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"},
+ "majic": {:hex, :majic, "1.2.0", "414b69c1460ece692fa892a0ed6669b8db6a44c42bb3071cb723854f22e7bd78", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0bd0c65f8e4dcc595757d957577f6151b59246da9614ba87debfdc641ccc0514"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"},
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"mua": {:hex, :mua, "0.2.4", "a9172ab0a1ac8732cf2699d739ceac3febcb9b4ffc540260ad2e32c0b6632af9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "e7e4dacd5ad65f13e3542772e74a159c00bd2d5579e729e9bb72d2c73a266fb7"},
"multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
"oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"},
"oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"},
- "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]},
+ "oban_plugins_lazarus": {:hex, :oban_plugins_lazarus, "0.1.1", "a5141d569e5b9f3bec8f4a958231d2c538af097d3c1ad06274f096fc06956821", [:mix], [{:oban, "< 3.0.0", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "7e9c51bc44d33f71c0a52cf0cd5c8d4c70380ede065c1042013f5123f2fc9729"},
"oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"},
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
"open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
- "phoenix": {:git, "https://github.com/feld/phoenix", "fb6dc76c657422e49600896c64aab4253fceaef6", [branch: "v1.7.14-websocket-headers"]},
+ "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
+ "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
"prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
"prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex.git", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]},
"prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"},
"prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
"quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
- "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
+ "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"},
"rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
"tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
"ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
"vix": {:hex, :vix, "0.36.0", "3132dc065beda06dab1895a53d8c852d8e6a5bbca375c609435e968b1290e113", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "92f912b4e90c453f92942742105bcdb367ad53406759da251bd2e587e33f4134"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
diff --git a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
index c24ab6751..2e12baa43 100644
--- a/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
+++ b/priv/gettext/fr/LC_MESSAGES/config_descriptions.po
@@ -1,6053 +1,6068 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-22 02:09+0300\n"
-"PO-Revision-Date: 2024-10-13 21:03+0000\n"
+"PO-Revision-Date: 2025-12-27 07:08+0000\n"
"Last-Translator: Codimp <contact@lithio.fr>\n"
"Language-Team: French <https://translate.pleroma.social/projects/pleroma/"
"pleroma-backend-domain-config_descriptions/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd"
msgid "Before enabling this you must add :esshd to mix.exs as one of the extra_applications and generate host keys in your priv dir with ssh-keygen -m PEM -N \"\" -b 2048 -t rsa -f ssh_host_rsa_key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger"
msgid "Logger-related settings"
msgstr "Paramètres liés à la journalisation"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :mime"
msgid "Mime Types settings"
msgstr "Paramètres des types Mime"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma"
msgid "Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)"
msgstr ""
"Permet de configurer un jeton qui peut être utilisé pour authentifier les "
"requêtes avec des privilèges administrateurs sans utiliser un jeton de "
"compte utilisateur standard. Pour l'utiliser, ajoutez le paramètre "
"`admin_token`aux requêtes. (Vous devriez utiliser l'authentification HTTP "
"Basic ou OAuth à la place si vous le pouvez)"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma"
msgid "Authenticator"
msgstr "Authentifieur"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :quack"
msgid "Quack-related settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug"
msgid "CORS plug config"
msgstr "Configuration du plug CORS"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd"
msgid "ESSHD"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger"
msgid "Logger"
msgstr "Journaliseur"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime"
msgid "Mime Types"
msgstr "Types Mime"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma"
msgid "Pleroma Admin Token"
msgstr "Jeton Administrateur Pleroma"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma"
msgid "Pleroma Authenticator"
msgstr "Authentifieur Pleroma"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :quack"
msgid "Quack Logger"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:console"
msgid "Console logger settings"
msgstr "Paramètres de journalisation de la console"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:ex_syslogger"
msgid "ExSyslogger-related settings"
msgstr "Paramètres liés à ExSyslogger"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub"
msgid "ActivityPub-related settings"
msgstr "Paramètres liés à ActivityPub"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:assets"
msgid "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend"
msgstr ""
"Cette section configure les annexes (assets) à utiliser avec divers "
"frontaux. La seule option est actuellement liée au mascottes du frontal "
"mastodon"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth"
msgid "Authentication / authorization settings"
msgstr "Paramètres d'authentification/autorisations"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool"
msgid "Advanced settings for `Gun` connections pool"
msgstr "Paramètres avancés pour le bac (pool) de connexions `Gun`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications"
msgid "Email notifications settings"
msgstr "Paramètres de notification par email"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:features"
msgid "Customizable features"
msgstr "Fonctionnalités personnalisables"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:feed"
msgid "Configure feed rendering"
msgstr "Configurer le rendu des flux"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations"
msgid "This form can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for pleroma_fe are configured. If you want to add your own configuration your settings all fields must be complete."
msgstr ""
"Ce formulaire peut être utilisé pour configurer une liste de clés (keyword) "
"qui contiennent les données de configuration pour tout types de frontaux. "
"Par défaut, les paramètres pour pleroma_fe sont configurés. Si vous voulez "
"ajouter vos propres paramètres de configurations, tout les champs doivent "
"être remplis."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends"
msgid "Installed frontends management"
msgstr "Gestion des frontaux installés"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher"
msgid "Gopher settings"
msgstr "Paramètres Gopher"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools"
msgid "Advanced settings for `Hackney` connections pools"
msgstr "Paramètres avancés pour les bacs (pool) de connexions `Hackney`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http"
msgid "HTTP settings"
msgstr "Paramètres HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security"
msgid "HTTP security settings"
msgstr "Paramètres de sécurité HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance"
msgid "Instance-related settings"
msgstr "Paramètres liés à l'instance"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instances_favicons"
msgid "Control favicons for instances"
msgstr "Gère les favicons des instances"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap"
msgid "Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password will be verified by trying to authenticate (bind) to a LDAP server. If a user exists in the LDAP directory but there is no account with the same name yet on the Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:majic_pool"
msgid "Majic/libmagic configuration"
msgstr "Configuration de majic/libmagic"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:manifest"
msgid "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE."
msgstr ""
"Cette section décrit les valeurs spécifique à l'instance du manifeste PWA. "
"Actuellement, cette option ne concerne que MastoFE."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy"
msgid "Media preview proxy"
msgstr "Proxy de prévisualisation média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy"
msgid "Media proxy"
msgstr "Proxy média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:modules"
msgid "Custom Runtime Modules"
msgstr "Modules Runtime Personalisés"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf"
msgid "General MRF settings"
msgstr "Paramètres généraux MRF"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_activity_expiration"
msgid "Adds automatic expiration to all local activities"
msgstr "Ajoute une expiration automatique à toutes les activités locales"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_follow_bot"
msgid "Automatically follows newly discovered accounts."
msgstr "Suivre automatiquement les comptes venant d'être découverts."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hashtag"
msgid "Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)\n\nNote: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.\n"
msgstr ""
"Rejeter, Enlever de TWKN ou marquer comme contenu sensible les messages avec "
"des mots-croisillons (sans mettre le # du début)\n"
"\n"
-"Note: cette politique MRF est toujours activée. Si vous voulez la "
+"Note : cette politique MRF est toujours activée. Si vous voulez la "
"désactiver, vous devez configurer des listes vides.\n"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hellthread"
msgid "Block messages with excessive user mentions"
msgstr "Bloquer les messages avec un nombre excessif de mentions"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_keyword"
msgid "Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html)."
msgstr ""
"Rejeter ou remplacer les mots des messages qui correspondent à un mot clef "
"ou à une [expression rationnelle (Regex)](https://hexdocs.pm/elixir/Regex."
"html)."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_mention"
msgid "Block messages which mention a specific user"
msgstr "Bloquer les messages mentionnant un utilisateur particulier"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_normalize_markup"
msgid "MRF NormalizeMarkup settings. Scrub configured hypertext markup."
msgstr ""
"Paramètres de normalisation MRF. Balaie les balises hypertextes configurées."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_object_age"
msgid "Rejects or delists posts based on their timestamp deviance from your server's clock."
msgstr ""
"Rejette ou retire des listes les messages selon l'écart entre leur heure et "
"l'horloge de votre serveur."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_rejectnonpublic"
msgid "RejectNonPublic drops posts with non-public visibility settings."
msgstr ""
"RejectNonPublic enlève les messages avec des paramètres de visibilité non-"
"publics."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple"
msgid "Simple ingress policies"
msgstr "Politiques simples pour entrants"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_steal_emoji"
msgid "Steals emojis from selected instances when it sees them."
msgstr "Vole les emojis des instances sélectionnées quand il les voit."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_subchain"
msgid "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules."
msgstr ""
"Cette politique traite les messages à travers un tuyau séparé lorsqu'un "
"message donné correspond à certain critères. Chaque critère est configuré "
"comme une correspondance entre une expression rationnelle et une liste de "
"modules de politiques."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_vocabulary"
msgid "Filter messages which belong to certain activity vocabularies"
msgstr ""
"Filtrer les messages qui correspondent à certain vocabulaires d'activités"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:oauth2"
msgid "Configure OAuth 2 provider capabilities"
msgstr "Configurer les capacités du fournisseur OAuth 2"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools"
msgid "Advanced settings for `Gun` workers pools"
msgstr "Paramètres avancés pour les bacs (pools) de travailleurs `Gun`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:populate_hashtags_table"
msgid "`populate_hashtags_table` background migration settings"
msgstr "Paramètres de migration en arrière-plan `populate_hashtags_table`"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit"
msgid "Rate limit settings. This is an advanced feature enabled only for :authentication by default."
msgstr ""
"Paramètres de limites par secondes. C'est une fonctionnalité avancée qui, "
"par défaut, n'est activée que pour :authentication."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated"
msgid "Disallow viewing timelines, user profiles and statuses for unauthenticated users."
msgstr ""
"Empêche de regarder les flux, les profils utilisateurs et les status pour "
"les utilisateurs non-authentifiés."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media"
msgid "If enabled the instance will parse metadata from attached links to generate link previews"
msgstr ""
"Si activé, l'instance interprétera les métadonnées des liens joins pour "
"générer les prévisualisations de liens"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:shout"
msgid "Pleroma shout settings"
-msgstr ""
+msgstr "Paramètre de Pleroma shout"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:static_fe"
msgid "Render profiles and posts using server-generated HTML that is viewable without using JavaScript"
msgstr ""
"Rendre les profils et les status en utilisant du HTML généré par le serveur "
"qui ne nécessitera pas de JavaScript"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:streamer"
msgid "Settings for notifications streamer"
-msgstr ""
+msgstr "Paramètres des streamers de notifications"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:uri_schemes"
msgid "URI schemes related settings"
msgstr "Paramètres liés au schémas d'URI"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:web_cache_ttl"
msgid "The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration."
msgstr ""
+"Temps d'expiration pour le cache des réponses web. Les valeurs doivent être "
+"en millisecondes ou `nil`pour désactiver l'expiration."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome"
msgid "Welcome messages settings"
-msgstr ""
+msgstr "Paramètres de messages de bienvenue"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:workers"
msgid "Includes custom worker options not interpretable directly by `Oban`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter"
msgid "Limits configuration for background tasks."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban"
msgid "[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha"
msgid "Captcha-related settings"
-msgstr ""
+msgstr "Paramètres liés au Captcha"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha"
msgid "Kocaptcha is a very simple captcha service with a single API endpoint, the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint (https://captcha.kotobank.ch) is hosted by the developer."
msgstr ""
+"Kocaptcha est un service de captcha très simple avec une API n'utilisant "
+"qu'une seule ressource, le code source est ici : https://github.com/"
+"koto-bank/kocaptcha. La ressource par défaut (https://captcha.kotobank.ch) "
+"est hébergée par son développeur."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer"
msgid "Mailer-related settings"
-msgstr ""
+msgstr "Paramètres liés à l'envoyeur d'email"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail"
msgid "New users admin email digest"
-msgstr ""
+msgstr "Email à l'administrateur de résumé des nouveaux utilisateurs"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail"
msgid "Email template settings"
-msgstr ""
+msgstr "Paramètres de template d'email"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter"
msgid "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs."
msgstr ""
+"Configuration du formateur de lien Pleroma qui interprète les mentions, "
+"hashtags et URLs."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.ScheduledActivity"
msgid "Scheduled activities settings"
-msgstr ""
+msgstr "Paramètres des activités planifiées"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload"
msgid "Upload general settings"
-msgstr ""
+msgstr "Paramètres généraux d'upload"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename"
msgid "Filter replaces the filename of the upload"
-msgstr ""
+msgstr "Filtre de remplacement du nom de fichier de l'upload"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify"
msgid "Uploads mogrify filter settings"
-msgstr ""
+msgstr "Paramètres de filtre mogrify pour les uploads"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.Local"
msgid "Local uploader-related settings"
-msgstr ""
+msgstr "Paramètres liés aux uploads locaux"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.S3"
msgid "S3 uploader-related settings"
-msgstr ""
+msgstr "Paramètres liés à l'uploader S3"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.User.Backup"
msgid "Account Backup"
-msgstr ""
+msgstr "Sauvegarde de Comptes"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http"
msgid "HTTP invalidate settings"
-msgstr ""
+msgstr "Paramètres d'invalidation HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script"
msgid "Invalidation script settings"
-msgstr ""
+msgstr "Paramètres du script d'invalidation"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Metadata"
msgid "Metadata-related settings"
-msgstr ""
+msgstr "Paramètres liés aux métadonnées"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp"
msgid "`Pleroma.Web.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.\n**If your instance is not behind at least one reverse proxy, you should not enable this plug.**\n"
msgstr ""
+"`Pleroma.Web.Plugs.RemoteIp` est un shim pour invoquer "
+"[`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) mais avec une "
+"configuration au runtime.\n"
+"**Si votre instance n'est pas derrière au moins un proxy inverse, vous ne "
+"devriez pas activer ce plug.**\n"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Preload"
msgid "Preload-related settings"
-msgstr ""
+msgstr "Paramètres liés au préchargement"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity"
msgid "Expired activities settings"
-msgstr ""
+msgstr "Paramètres des activités expirées"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter"
msgid "Prometheus app metrics endpoint configuration"
-msgstr ""
+msgstr "Configuration des endpoints de métriques Prometheus"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :web_push_encryption-:vapid_details"
msgid "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it."
msgstr ""
+"Configuration des notifications Web Push. Vous pouvez utiliser la tâche mix "
+"web_push.gen.keypair pour le générer."
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :ex_aws-:s3"
msgid "S3"
-msgstr ""
+msgstr "S3"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:console"
msgid "Console Logger"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:ex_syslogger"
msgid "ExSyslogger"
-msgstr ""
+msgstr "ExSyslogger"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub"
msgid "ActivityPub"
-msgstr ""
+msgstr "ActivityPub"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:assets"
msgid "Assets"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:auth"
msgid "Auth"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool"
msgid "Connections pool"
-msgstr ""
+msgstr "Pool de connexions"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications"
msgid "Email notifications"
-msgstr ""
+msgstr "Notifications email"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji"
msgid "Emoji"
-msgstr ""
+msgstr "Emoji"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:features"
msgid "Features"
-msgstr ""
+msgstr "Fonctionnalités"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:feed"
msgid "Feed"
-msgstr ""
+msgstr "Flux"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations"
msgid "Frontend configurations"
-msgstr ""
+msgstr "Configurations des frontends"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends"
msgid "Frontends"
-msgstr ""
+msgstr "Frontends"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:gopher"
msgid "Gopher"
-msgstr ""
+msgstr "Gopher"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools"
msgid "Hackney pools"
-msgstr ""
+msgstr "Bacs (pools) d'Hackney"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http"
msgid "HTTP"
-msgstr ""
+msgstr "HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security"
msgid "HTTP security"
-msgstr ""
+msgstr "Sécurité HTTP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance"
msgid "Instance"
-msgstr ""
+msgstr "Instance"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instances_favicons"
msgid "Instances favicons"
-msgstr ""
+msgstr "Favicons des instances"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap"
msgid "LDAP"
-msgstr ""
+msgstr "LDAP"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:majic_pool"
msgid "Majic pool"
-msgstr ""
+msgstr "Bac (pool) de Majic"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:manifest"
msgid "Manifest"
-msgstr ""
+msgstr "Manifeste"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup"
msgid "Markup Settings"
-msgstr ""
+msgstr "Paramètres des Balises"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy"
msgid "Media preview proxy"
-msgstr ""
+msgstr "Proxy de prévisualisation média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy"
msgid "Media proxy"
-msgstr ""
+msgstr "Proxy média"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:modules"
msgid "Modules"
-msgstr ""
+msgstr "Modules"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf"
msgid "MRF"
-msgstr ""
+msgstr "MRF"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_activity_expiration"
msgid "MRF Activity Expiration Policy"
-msgstr ""
+msgstr "Politique MRF d'Expiration des Activités"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_follow_bot"
msgid "MRF FollowBot Policy"
-msgstr ""
+msgstr "Politique MRF FollowBot"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hashtag"
msgid "MRF Hashtag"
-msgstr ""
+msgstr "Politique MRF hashtag"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hellthread"
msgid "MRF Hellthread"
-msgstr ""
+msgstr "MRF Hellthread"
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_keyword"
msgid "MRF Keyword"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_mention"
msgid "MRF Mention"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_normalize_markup"
msgid "MRF Normalize Markup"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_object_age"
msgid "MRF Object Age"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_rejectnonpublic"
msgid "MRF Reject Non Public"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple"
msgid "MRF Simple"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_steal_emoji"
msgid "MRF Emojis"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_subchain"
msgid "MRF Subchain"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_vocabulary"
msgid "MRF Vocabulary"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:oauth2"
msgid "OAuth2"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools"
msgid "Pools"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:populate_hashtags_table"
msgid "Populate hashtags table"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit"
msgid "Rate limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated"
msgid "Restrict Unauthenticated"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media"
msgid "Rich media"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:shout"
msgid "Shout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:static_fe"
msgid "Static FE"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:streamer"
msgid "Streamer"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:uri_schemes"
msgid "URI Schemes"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:user"
msgid "User"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:web_cache_ttl"
msgid "Web cache TTL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome"
msgid "Welcome"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:workers"
msgid "Workers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter"
msgid "ConcurrentLimiter"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban"
msgid "Oban"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha"
msgid "Pleroma.Captcha"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha"
msgid "Pleroma.Captcha.Kocaptcha"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer"
msgid "Pleroma.Emails.Mailer"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.NewUsersDigestEmail"
msgid "Pleroma.Emails.NewUsersDigestEmail"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail"
msgid "Pleroma.Emails.UserEmail"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter"
msgid "Linkify"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.ScheduledActivity"
msgid "Pleroma.ScheduledActivity"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload"
msgid "Pleroma.Upload"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename"
msgid "Pleroma.Upload.Filter.AnonymizeFilename"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload.Filter.Mogrify"
msgid "Pleroma.Upload.Filter.Mogrify"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.Local"
msgid "Pleroma.Uploaders.Local"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.S3"
msgid "Pleroma.Uploaders.S3"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User"
msgid "Pleroma.User"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User.Backup"
msgid "Pleroma.User.Backup"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate"
msgid "Pleroma.Web.ApiSpec.CastAndValidate"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http"
msgid "Pleroma.Web.MediaProxy.Invalidation.Http"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script"
msgid "Pleroma.Web.MediaProxy.Invalidation.Script"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Metadata"
msgid "Pleroma.Web.Metadata"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp"
msgid "Pleroma.Web.Plugs.RemoteIp"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Preload"
msgid "Pleroma.Web.Preload"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity"
msgid "Pleroma.Workers.PurgeExpiredActivity"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter"
msgid "Pleroma.Web.Endpoint.MetricsExporter"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :web_push_encryption-:vapid_details"
msgid "Vapid Details"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd > :enabled"
msgid "Enables SSH"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd > :handler"
msgid "Handler module"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd > :password_authenticator"
msgid "Authenticator module"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd > :port"
msgid "Port to connect"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :esshd > :priv_dir"
msgid "Dir with SSH keys"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :ex_aws-:s3 > :access_key_id"
msgid "S3 access key ID"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :ex_aws-:s3 > :host"
msgid "S3 host"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :ex_aws-:s3 > :region"
msgid "S3 region (for AWS)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :ex_aws-:s3 > :secret_access_key"
msgid "Secret access key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger > :backends"
msgid "Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :ex_syslogger } - to syslog, Quack.Logger - to Slack."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:console > :format"
msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:console > :level"
msgid "Log level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:ex_syslogger > :format"
msgid "Default: \"$date $time [$level] $levelpad$node $metadata $message\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:ex_syslogger > :ident"
msgid "A string that's prepended to every message, and is typically set to the app name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :logger-:ex_syslogger > :level"
msgid "Log level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma > :admin_token"
msgid "Admin token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :blockers_visible"
msgid "Whether a user can see someone who has blocked them"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :follow_handshake_timeout"
msgid "Following handshake timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :note_replies_output_limit"
msgid "The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :outgoing_blocks"
msgid "Whether to federate blocks to other instances"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :sign_object_fetches"
msgid "Sign object fetches with HTTP signatures"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:activitypub > :unfollow_blocked"
msgid "Whether blocks result in people getting unfollowed"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:assets > :default_mascot"
msgid "This will be used as the default mascot on MastoFE. Default: `:pleroma_fox_tan`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:assets > :default_user_avatar"
msgid "URL of the default user avatar"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:assets > :mascots"
msgid "Keyword of mascots, each element must contain both an URL and a mime_type key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth > :auth_template"
msgid "Authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.ee`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth > :enforce_oauth_admin_scope_usage"
msgid "OAuth admin scope requirement toggle. If enabled, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes). If disabled and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth > :oauth_consumer_strategies"
msgid "The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-delimited string should be of format \"strategy\" or \"strategy:dependency\" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:auth > :oauth_consumer_template"
msgid "OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool > :connect_timeout"
msgid "Timeout while `gun` will wait until connection is up. Default: 5000ms."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool > :connection_acquisition_retries"
msgid "Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool > :connection_acquisition_wait"
msgid "Timeout to acquire a connection from pool. The total max time is this value multiplied by the number of retries. Default: 250ms."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool > :max_connections"
msgid "Maximum number of connections in the pool. Default: 250 connections."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:connections_pool > :reclaim_multiplier"
msgid "Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications > :digest"
msgid "emails of \"what you've missed\" for users who have been inactive for a while"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications > :digest > :active"
msgid "Globally enable or disable digest emails"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications > :digest > :inactivity_threshold"
msgid "Minimum user inactivity threshold"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications > :digest > :interval"
msgid "Minimum interval between digest emails to one user"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:email_notifications > :digest > :schedule"
msgid "When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\"."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:emoji > :default_manifest"
msgid "Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:emoji > :groups"
msgid "Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name and the value is the location or array of locations. * can be used as a wildcard."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:emoji > :pack_extensions"
msgid "A list of file extensions for emojis, when no emoji.txt for a pack is present"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:emoji > :shortcode_globs"
msgid "Location of custom emoji files. * can be used as a wildcard."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:features > :improved_hashtag_timeline"
msgid "Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:feed > :post_title"
msgid "Configure title rendering"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:feed > :post_title > :max_length"
msgid "Maximum number of characters before truncating title"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:feed > :post_title > :omission"
msgid "Replacement which will be used after truncating string"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe"
msgid "Settings for Pleroma FE"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :alwaysShowSubjectInput"
msgid "When disabled, auto-hide the subject field if it's empty"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :background"
msgid "URL of the background, unless viewing a user profile with a background that is set"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :collapseMessageWithSubject"
msgid "When a message has a subject (aka Content Warning), collapse it by default"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :greentext"
msgid "Enables green text on lines prefixed with the > character"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideFilteredStatuses"
msgid "Hides filtered statuses from timelines"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideMutedPosts"
msgid "Hides muted statuses from timelines"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hidePostStats"
msgid "Hide notices statistics (repeats, favorites, ...)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideSitename"
msgid "Hides instance name from PleromaFE banner"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :hideUserStats"
msgid "Hide profile statistics (posts, posts per day, followers, followings, ...)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logo"
msgid "URL of the logo, defaults to Pleroma's logo"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logoMargin"
msgid "Allows you to adjust vertical margins between logo boundary and navbar borders. The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :logoMask"
msgid "By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. If you want a colorful logo you must disable logoMask."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :minimalScopesMode"
msgid "Limit scope selection to Direct, User default, and Scope of post replying to. Also prevents replying to a DM with a public post from PleromaFE."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :nsfwCensorImage"
msgid "URL of the image to use for hiding NSFW media attachments in the timeline"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :postContentType"
msgid "Default post formatting option"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootLogin"
msgid "Relative URL which indicates where to redirect when a user is logged in"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootNoLogin"
msgid "Relative URL which indicates where to redirect when a user isn't logged in"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :scopeCopy"
msgid "Copy the scope (private/unlisted/public) in replies to posts by default"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :showFeaturesPanel"
msgid "Enables panel displaying functionality of the instance on the About page"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :showInstanceSpecificPanel"
msgid "Whether to show the instance's custom panel"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :sidebarRight"
msgid "Change alignment of sidebar and panels to the right"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :subjectLineBehavior"
msgid "Allows changing the default behaviour of subject lines in replies.\n `email`: copy and preprend re:, as in email,\n `masto`: copy verbatim, as in Mastodon,\n `noop`: don't copy the subject."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontend_configurations > :pleroma_fe > :theme"
msgid "Which theme to use. Available themes are defined in styles.json"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :admin"
msgid "Admin frontend"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :admin > name"
msgid "Name of the installed frontend. Valid config must include both `Name` and `Reference` values."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :admin > ref"
msgid "Reference of the installed frontend to be used. Valid config must include both `Name` and `Reference` values."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available"
msgid "A map containing available frontends and parameters for their installation."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > build_dir"
msgid "The directory inside the zip file "
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > build_url"
msgid "Either an url to a zip file containing the frontend or a template to build it by inserting the `ref`. The string `${ref}` will be replaced by the configured `ref`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > custom-http-headers"
msgid "The custom HTTP headers for the frontend"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > git"
msgid "URL of the git repository of the frontend"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > name"
msgid "Name of the frontend."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :available > ref"
msgid "Reference of the frontend to be used."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :primary"
msgid "Primary frontend, the one that is served for all pages by default"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :primary > name"
msgid "Name of the installed frontend. Valid config must include both `Name` and `Reference` values."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:frontends > :primary > ref"
msgid "Reference of the installed frontend to be used. Valid config must include both `Name` and `Reference` values."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher > :dstport"
msgid "Port advertised in URLs (optional, defaults to port)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher > :enabled"
msgid "Enables the gopher interface"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher > :ip"
msgid "IP address to bind to"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:gopher > :port"
msgid "Port to bind to"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :federation"
msgid "Settings for federation pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :federation > :max_connections"
msgid "Number workers in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :federation > :timeout"
msgid "Timeout while `hackney` will wait for response."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :media"
msgid "Settings for media pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :media > :max_connections"
msgid "Number workers in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :media > :timeout"
msgid "Timeout while `hackney` will wait for response."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :upload"
msgid "Settings for upload pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :upload > :max_connections"
msgid "Number workers in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:hackney_pools > :upload > :timeout"
msgid "Timeout while `hackney` will wait for response."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http > :adapter"
msgid "Adapter specific options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http > :adapter > :ssl_options"
msgid "SSL options for HTTP adapter"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http > :adapter > :ssl_options > :versions"
msgid "List of TLS version to use"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http > :proxy_url"
msgid "Proxy URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http > :user_agent"
msgid "What user agent to use. Must be a string or an atom `:default`. Default value is `:default`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :ct_max_age"
msgid "The maximum age for the Expect-CT header if sent"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :enabled"
msgid "Whether the managed content security policy is enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :referrer_policy"
msgid "The referrer policy to use, either \"same-origin\" or \"no-referrer\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :report_uri"
msgid "Adds the specified URL to report-uri and report-to group in CSP header"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :sts"
msgid "Whether to additionally send a Strict-Transport-Security header"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:http_security > :sts_max_age"
msgid "The maximum age for the Strict-Transport-Security header if sent"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :account_activation_required"
msgid "Require users to confirm their emails before signing in"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :account_approval_required"
msgid "Require users to be manually approved by an admin before signing in"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :account_field_name_length"
msgid "An account field name maximum length. Default: 512."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :account_field_value_length"
msgid "An account field value maximum length. Default: 2048."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :allow_relay"
msgid "Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :allowed_post_formats"
msgid "MIME-type list of formats allowed to be posted (transformed into HTML)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :attachment_links"
msgid "Enable to automatically add attachment link text to statuses"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :autofollowed_nicknames"
msgid "Set to nicknames of (local) users that every new user should automatically follow"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :autofollowing_nicknames"
msgid "Set to nicknames of (local) users that automatically follows every newly registered user"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :avatar_upload_limit"
msgid "File size limit of user's profile avatars"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :background_upload_limit"
msgid "File size limit of user's profile backgrounds"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :banner_upload_limit"
msgid "File size limit of user's profile banners"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :birthday_min_age"
msgid "Minimum required age for users to create account. Only used if birthday is required."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :birthday_required"
msgid "Require users to enter their birthday."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :cleanup_attachments"
msgid "Enable to remove associated attachments when status is removed.\nThis will not affect duplicates and attachments without status.\nEnabling this will increase load to database when deleting statuses on larger instances.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :description"
msgid "The instance's description. It can be seen in nodeinfo and `/api/v1/instance`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :email"
msgid "Email used to reach an Administrator/Moderator of the instance"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :extended_nickname_format"
msgid "Enable to use extended local nicknames format (allows underscores/dashes). This will break federation with older software for theses nicknames."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :external_user_synchronization"
msgid "Enabling following/followers counters synchronization for external users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :federating"
msgid "Enable federation with other instances"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :federation_incoming_replies_max_depth"
msgid "Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :federation_reachability_timeout_days"
msgid "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :healthcheck"
msgid "If enabled, system data will be shown on `/api/pleroma/healthcheck`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :instance_thumbnail"
msgid "The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :invites_enabled"
msgid "Enable user invitations for admins (depends on `registrations_open` being disabled)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :limit"
msgid "Posts character limit (CW/Subject included in the counter)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :limit_to_local_content"
msgid "Limit unauthenticated users to search for local statutes and users only. Default: `:unauthenticated`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_account_fields"
msgid "The maximum number of custom fields in the user profile. Default: 10."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_endorsed_users"
msgid "The maximum number of recommended accounts. 0 will disable the feature."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_media_attachments"
msgid "Maximum number of post media attachments"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_pinned_statuses"
msgid "The maximum number of pinned statuses. 0 will disable the feature."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_remote_account_fields"
msgid "The maximum number of custom fields in the remote user profile. Default: 20."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :max_report_comment_size"
msgid "The maximum size of the report comment. Default: 1000."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication"
msgid "Multi-factor authentication settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes"
msgid "MFA backup codes settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :length"
msgid "Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :number"
msgid "Number of backup codes to generate."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp"
msgid "TOTP settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp > :digits"
msgid "Determines the length of a one-time pass-code, in characters. Defaults to 6 characters."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :multi_factor_authentication > :totp > :period"
msgid "A period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :name"
msgid "Name of the instance"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :notify_email"
msgid "Envelope FROM address for mail sent via Pleroma"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :poll_limits"
msgid "A map with poll limits for local polls"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :poll_limits > :max_expiration"
msgid "Maximum expiration time (in seconds)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :poll_limits > :max_option_chars"
msgid "Maximum number of characters per option"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :poll_limits > :max_options"
msgid "Maximum number of options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :poll_limits > :min_expiration"
msgid "Minimum expiration time (in seconds)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :privileged_staff"
msgid "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :profile_directory"
msgid "Enable profile directory."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :public"
msgid "Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note: when setting to `false`, please also check `:restrict_unauthenticated` setting."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :quarantined_instances"
msgid "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :registration_reason_length"
msgid "Maximum registration reason length. Default: 500."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :registrations_open"
msgid "Enable registrations for anyone. Invitations require this setting to be disabled."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :remote_limit"
msgid "Hard character limit beyond which remote posts will be dropped"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :remote_post_retention_days"
msgid "The default amount of days to retain remote posts when pruning the database"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :safe_dm_mentions"
msgid "If enabled, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@admin please keep an eye on @bad_actor\"). Default: disabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :show_reactions"
msgid "Let favourites and emoji reactions be viewed through the API."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :skip_thread_containment"
msgid "Skip filtering out broken threads. Default: enabled."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :static_dir"
msgid "Instance static directory"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :upload_limit"
msgid "File size limit of uploads (except for avatar, background, banner)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :user_bio_length"
msgid "A user bio maximum length. Default: 5000."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instance > :user_name_length"
msgid "A user name maximum length. Default: 100."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:instances_favicons > :enabled"
msgid "Allow/disallow displaying and getting instances favicons"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :base"
msgid "LDAP base, e.g. \"dc=example,dc=com\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :enabled"
msgid "Enables LDAP authentication"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :host"
msgid "LDAP server hostname"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :port"
msgid "LDAP port, e.g. 389 or 636"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :ssl"
msgid "Enable to use SSL, usually implies the port 636"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :sslopts"
msgid "Additional SSL options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :sslopts > :cacertfile"
msgid "Path to file with PEM encoded cacerts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :sslopts > :verify"
msgid "Type of cert verification"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :tls"
msgid "Enable to use STARTTLS, usually implies the port 389"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :tlsopts"
msgid "Additional TLS options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :tlsopts > :cacertfile"
msgid "Path to file with PEM encoded cacerts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :tlsopts > :verify"
msgid "Type of cert verification"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:ldap > :uid"
msgid "LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:majic_pool > :size"
msgid "Number of majic workers to start."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:manifest > :background_color"
msgid "Describe the background color of the app"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:manifest > :icons"
msgid "Describe the icons of the app"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:manifest > :theme_color"
msgid "Describe the theme color of the app"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:markup > :scrub_policy"
msgid "Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy > :enabled"
msgid "Enables proxying of remote media preview to the instance's proxy. Requires enabled media proxy."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy > :image_quality"
msgid "Quality of the output. Ranges from 0 (min quality) to 100 (max quality)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy > :min_content_length"
msgid "Min content length (in bytes) to perform preview. Media smaller in size will be served without thumbnailing."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy > :thumbnail_max_height"
msgid "Max height of preview thumbnail for images (video preview always has original dimensions)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_preview_proxy > :thumbnail_max_width"
msgid "Max width of preview thumbnail for images (video preview always has original dimensions)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :base_url"
msgid "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :enabled"
msgid "Enables proxying of remote media via the instance's proxy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :invalidation > :enabled"
msgid "Enables media cache object invalidation."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :invalidation > :provider"
msgid "Module which will be used to purge objects from the cache."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :proxy_opts"
msgid "Internal Pleroma.ReverseProxy settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :max_body_length"
msgid "Maximum file size (in bytes) allowed through the Pleroma MediaProxy cache."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :max_read_duration"
msgid "Timeout (in milliseconds) of GET request to the remote URI."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :proxy_opts > :redirect_on_failure"
msgid "Redirects the client to the origin server upon encountering HTTP errors.\n\nNote that files larger than Max Body Length will trigger an error. (e.g., Peertube videos)\n\n\n**WARNING:** This setting will allow larger files to be accessed, but exposes the\n\nIP addresses of your users to the other servers, bypassing the MediaProxy.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:media_proxy > :whitelist"
msgid "List of hosts with scheme to bypass the MediaProxy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:modules > :runtime_dir"
msgid "A path to custom Elixir modules (such as MRF policies)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf > :policies"
msgid "A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf > :transparency"
msgid "Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf > :transparency_exclusions"
msgid "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. You can also provide a reason for excluding these instance names. The instances and reasons won't be publicly disclosed."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_activity_expiration > :days"
msgid "Default global expiration time for all local activities (in days)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_follow_bot > :follower_nickname"
msgid "The name of the bot account to use for following newly discovered users."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hashtag > :federated_timeline_removal"
msgid "A list of hashtags which result in message being removed from federated timelines (a.k.a unlisted)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hashtag > :reject"
msgid "A list of hashtags which result in message being rejected."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hashtag > :sensitive"
msgid "A list of hashtags which result in message being set as sensitive (a.k.a NSFW/R-18)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hellthread > :delist_threshold"
msgid "Number of mentioned users after which the message gets removed from timelines anddisables notifications. Set to 0 to disable."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_hellthread > :reject_threshold"
msgid "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_keyword > :federated_timeline_removal"
msgid " A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_keyword > :reject"
msgid " A list of patterns which result in message being rejected.\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_keyword > :replace"
msgid " **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n\n **Replacement**: a string. Leaving the field empty is permitted.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_mention > :actors"
msgid "A list of actors for which any post mentioning them will be dropped"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_object_age > :actions"
msgid "A list of actions to apply to the post. `:delist` removes the post from public timelines; `:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; `:reject` rejects the message entirely"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_object_age > :threshold"
msgid "Required age (in seconds) of a post before actions are taken."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_rejectnonpublic > :allow_direct"
msgid "Whether to allow direct messages"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_rejectnonpublic > :allow_followersonly"
msgid "Whether to allow followers-only posts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :accept"
msgid "List of instances to only accept activities from (except deletes) and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :avatar_removal"
msgid "List of instances to strip avatars from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :banner_removal"
msgid "List of instances to strip banners from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :federated_timeline_removal"
msgid "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :followers_only"
msgid "Force posts from the given instances to be visible by followers only and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :media_nsfw"
msgid "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :media_removal"
msgid "List of instances to strip media attachments from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :reject"
msgid "List of instances to reject activities from (except deletes) and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :reject_deletes"
msgid "List of instances to reject deletions from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_simple > :report_removal"
msgid "List of instances to reject reports from and the reason for doing so"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_steal_emoji > :hosts"
msgid "List of hosts to steal emojis from"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_steal_emoji > :rejected_shortcodes"
msgid " A list of patterns or matches to reject shortcodes with.\n\n Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_steal_emoji > :size_limit"
msgid "File size limit (in bytes), checked before an emoji is saved to the disk"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_subchain > :match_actor"
msgid "Matches a series of regular expressions against the actor field"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_vocabulary > :accept"
msgid "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:mrf_vocabulary > :reject"
msgid "A list of ActivityStreams terms to reject. If empty, no messages are rejected."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:oauth2 > :clean_expired_tokens"
msgid "Enable a background job to clean expired OAuth tokens. Default: disabled."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:oauth2 > :issue_new_refresh_token"
msgid "Keeps old refresh token or generate new refresh token when to obtain an access token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:oauth2 > :token_expires_in"
msgid "The lifetime in seconds of the access token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :default"
msgid "Settings for default pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :default > :max_waiting"
msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :default > :recv_timeout"
msgid "Timeout for the pool while gun will wait for response"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :default > :size"
msgid "Maximum number of concurrent requests in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :federation"
msgid "Settings for federation pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :federation > :max_waiting"
msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :federation > :recv_timeout"
msgid "Timeout for the pool while gun will wait for response"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :federation > :size"
msgid "Maximum number of concurrent requests in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :media"
msgid "Settings for media pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :media > :max_waiting"
msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :media > :recv_timeout"
msgid "Timeout for the pool while gun will wait for response"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :media > :size"
msgid "Maximum number of concurrent requests in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :upload"
msgid "Settings for upload pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :upload > :max_waiting"
msgid "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :upload > :recv_timeout"
msgid "Timeout for the pool while gun will wait for response"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:pools > :upload > :size"
msgid "Maximum number of concurrent requests in the pool."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:populate_hashtags_table > :fault_rate_allowance"
msgid "Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if hashtags transfer failed for all records."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:populate_hashtags_table > :sleep_interval_ms"
msgid "Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :app_account_creation"
msgid "For registering user accounts from the same IP address"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :authentication"
msgid "For authentication create / password check / user existence check requests"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :relation_id_action"
msgid "For actions on relation with a specific user (follow, unfollow)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :relations_actions"
msgid "For actions on relationships with all users (follow, unfollow)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :search"
msgid "For the search requests (account & status search etc.)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :status_id_action"
msgid "For fav / unfav or reblog / unreblog actions on the same status by the same user"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :statuses_actions"
msgid "For create / delete / fav / unfav / reblog / unreblog actions on any statuses"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rate_limit > :timeline"
msgid "For requests to timelines (each timeline has it's own limiter)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities"
msgid "Settings for statuses."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities > :local"
msgid "Disallow view local statuses."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :activities > :remote"
msgid "Disallow view remote statuses."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles"
msgid "Settings for user profiles."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles > :local"
msgid "Disallow view local user profiles."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :profiles > :remote"
msgid "Disallow view remote user profiles."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines"
msgid "Settings for public and federated timelines."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines > :federated"
msgid "Disallow view federated timeline."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:restrict_unauthenticated > :timelines > :local"
msgid "Disallow view public timeline."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :enabled"
msgid "Enables RichMedia parsing of URLs"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :failure_backoff"
msgid "Amount of milliseconds after request failure, during which the request will not be retried."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :ignore_hosts"
msgid "List of hosts which will be ignored by the metadata parser"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :ignore_tld"
msgid "List TLDs (top-level domains) which will ignore for parse metadata"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :parsers"
msgid "List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:rich_media > :ttl_setters"
msgid "List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:shout > :enabled"
msgid "Enables the backend Shoutbox chat feature."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:shout > :limit"
msgid "Shout message character limit."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:static_fe > :enabled"
msgid "Enables the rendering of static HTML. Default: disabled."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:streamer > :overflow_workers"
msgid "Maximum number of workers created if pool is empty"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:streamer > :workers"
msgid "Number of workers to send notifications"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:uri_schemes > :valid_schemes"
msgid "List of the scheme part that is considered valid to be an URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:web_cache_ttl > :activity_pub"
msgid "Activity pub routes (except question activities). Default: `nil` (no expiration)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:web_cache_ttl > :activity_pub_question"
msgid "Activity pub routes (question activities). Default: `30_000` (30 seconds)."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :chat_message > :enabled"
msgid "Enables sending a chat message to newly registered users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :chat_message > :message"
msgid "A message that will be sent to newly registered users as a chat message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :chat_message > :sender_nickname"
msgid "The nickname of the local user that sends a welcome chat message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :direct_message > :enabled"
msgid "Enables sending a direct message to newly registered users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :direct_message > :message"
msgid "A message that will be sent to newly registered users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :direct_message > :sender_nickname"
msgid "The nickname of the local user that sends a welcome message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :email > :enabled"
msgid "Enables sending an email to newly registered users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :email > :html"
msgid "HTML content of the welcome email. EEX template with user and instance_name variables can be used."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :email > :sender"
msgid "Email address and/or nickname that will be used to send the welcome email."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :email > :subject"
msgid "Subject of the welcome email. EEX template with user and instance_name variables can be used."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:welcome > :email > :text"
msgid "Text content of the welcome email. EEX template with user and instance_name variables can be used."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-:workers > :retries"
msgid "Max retry attempts for failed jobs, per `Oban` queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
msgid "Concurrent limits configuration for MediaProxyWarmingPolicy."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_running"
msgid "Max running concurrently jobs."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_waiting"
msgid "Max waiting jobs."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers"
msgid "Concurrent limits configuration for getting RichMedia for activities."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_running"
msgid "Max running concurrently jobs."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_waiting"
msgid "Max waiting jobs."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :crontab"
msgid "Settings for cron background jobs"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :log"
msgid "Logs verbose mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues"
msgid "Background jobs queues (keys: queues, values: max numbers of concurrent jobs)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :activity_expiration"
msgid "Activity expiration queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :attachments_cleanup"
msgid "Attachment deletion queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :background"
msgid "Background queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :backup"
msgid "Backup queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :federator_incoming"
msgid "Incoming federation queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :federator_outgoing"
msgid "Outgoing federation queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :mailer"
msgid "Email sender queue, see Pleroma.Emails.Mailer"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :scheduled_activities"
msgid "Scheduled activities queue, see Pleroma.ScheduledActivities"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :transmogrifier"
msgid "Transmogrifier queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Oban > :queues > :web_push"
msgid "Web push notifications queue"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha > :enabled"
msgid "Whether the captcha should be shown on registration"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha > :method"
msgid "The method/service to use for captcha"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha > :seconds_valid"
msgid "The time in seconds for which the captcha is valid"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Captcha.Kocaptcha > :endpoint"
msgid "The kocaptcha endpoint to use"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > :adapter"
msgid "One of the mail adapters listed in [Swoosh documentation](https://hexdocs.pm/swoosh/Swoosh.html#module-adapters)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:auth"
msgid "SMTP AUTH enforcement mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:password"
msgid "SMTP AUTH password"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:port"
msgid "SMTP port"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:relay"
msgid "Hostname or IP address"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:retries"
msgid "SMTP temporary (4xx) error retries"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:ssl"
msgid "Use Implicit SSL/TLS. e.g. port 465"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:tls"
msgid "Explicit TLS (STARTTLS) enforcement mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:username"
msgid "SMTP AUTH username"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.NewUsersDigestEmail > :enabled"
msgid "Enables new users admin digest email when `true`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail > :logo"
msgid "A path to a custom logo. Set it to `nil` to use the default Pleroma logo."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Emails.UserEmail > :styling"
msgid "A map with color settings for email templates."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :class"
msgid "Specify the class to be added to the generated link. Disable to clear."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :extra"
msgid "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :new_window"
msgid "Link URLs will open in a new window/tab."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :rel"
msgid "Override the rel attribute. Disable to clear."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :strip_prefix"
msgid "Strip the scheme prefix."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :truncate"
msgid "Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Formatter > :validate_tld"
msgid "Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :daily_user_limit"
msgid "The number of scheduled activities a user is allowed to create in a single day. Default: 25."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :enabled"
msgid "Whether scheduled activities are sent to the job queue to be executed"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.ScheduledActivity > :total_user_limit"
msgid "The number of scheduled activities a user is allowed to create in total. Default: 300."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :base_url"
msgid "Base URL for the uploads. Required if you use a CDN or host attachments under a different domain."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :filename_display_max_length"
msgid "Set max length of a filename to display. 0 = no limit. Default: 30"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :filters"
msgid "List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :link_name"
msgid "If enabled, a name parameter will be added to the URL of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :proxy_remote"
msgid "Proxy requests to the remote uploader.\n\nUseful if media upload endpoint is not internet accessible.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload > :uploader"
msgid "Module which will be used for uploads"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename > :text"
msgid "Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Upload.Filter.Mogrify > :args"
msgid "List of actions for the mogrify command. It's possible to add self-written settings as string. For example `auto-orient, strip, {\"resize\", \"3840x1080>\"}` value will be parsed into valid list of the settings."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.Local > :uploads"
msgid "Path where user's uploads will be saved"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :bucket"
msgid "S3 bucket"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :bucket_namespace"
msgid "S3 bucket namespace"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :streaming_enabled"
msgid "Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Uploaders.S3 > :truncated_namespace"
msgid "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc. For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in Upload base_url."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.User > :email_blacklist"
msgid "List of email domains users may not register with."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.User > :restricted_nicknames"
msgid "List of nicknames users may not register with."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.User.Backup > :limit_days"
msgid "Limit user to export not more often than once per N days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.User.Backup > :purge_after_days"
msgid "Remove backup achives after N days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate > :strict"
msgid "Enables strict input validation (useful in development, not recommended in production)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :headers"
msgid "HTTP headers of request"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :method"
msgid "HTTP method of request. Default: :purge"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options"
msgid "Request options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :script_path"
msgid "Path to executable script which will purge cached items."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :url_format"
msgid "Optional URL format preprocessing. Only required for Apache's htcacheclean."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Metadata > :providers"
msgid "List of metadata providers to enable"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Metadata > :unfurl_nsfw"
msgid "When enabled NSFW attachments will be shown in previews"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :enabled"
msgid "Enable/disable the plug. Default: disabled."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :headers"
msgid " A list of strings naming the HTTP headers to use when deriving the true client IP. Default: `[\"x-forwarded-for\"]`.\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :proxies"
msgid "A list of upstream proxy IP subnets in CIDR notation from which we will parse the content of `headers`. Defaults to `[]`. IPv4 entries without a bitmask will be assumed to be /32 and IPv6 /128."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Plugs.RemoteIp > :reserved"
msgid " A list of reserved IP subnets in CIDR notation which should be ignored if found in `headers`. Defaults to `[\"127.0.0.0/8\", \"::1/128\", \"fc00::/7\", \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\"]`\n"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Web.Preload > :providers"
msgid "List of preload providers to enable"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :enabled"
msgid "Enables expired activities addition & deletion"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :min_lifetime"
msgid "Minimum lifetime for ephemeral activity (in seconds)"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :auth"
msgid "Enables HTTP Basic Auth for app metrics endpoint."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :enabled"
msgid "[Pleroma extension] Enables app metrics endpoint."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :format"
msgid "App metrics endpoint output format."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :ip_whitelist"
msgid "Restrict access of app metrics endpoint to the specified IP addresses."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :path"
msgid "App metrics endpoint URI path."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :quack > :level"
msgid "Log level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :quack > :meta"
msgid "Configure which metadata you want to report on"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :quack > :webhook_url"
msgid "Configure the Slack incoming webhook"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :web_push_encryption-:vapid_details > :private_key"
msgid "VAPID private key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :web_push_encryption-:vapid_details > :public_key"
msgid "VAPID public key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config description at :web_push_encryption-:vapid_details > :subject"
msgid "A mailto link for the administrative contact. It's best if this email is not a personal email address, but rather a group email to the instance moderation team."
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug > :credentials"
msgid "Credentials"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug > :expose"
msgid "Expose"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug > :headers"
msgid "Headers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug > :max_age"
msgid "Max age"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :cors_plug > :methods"
msgid "Methods"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd > :handler"
msgid "Handler"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd > :password_authenticator"
msgid "Password authenticator"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd > :port"
msgid "Port"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :esshd > :priv_dir"
msgid "Priv dir"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :ex_aws-:s3 > :access_key_id"
msgid "Access key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :ex_aws-:s3 > :host"
msgid "Host"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :ex_aws-:s3 > :region"
msgid "Region"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :ex_aws-:s3 > :secret_access_key"
msgid "Secret access key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger > :backends"
msgid "Backends"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:console > :format"
msgid "Format"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:console > :level"
msgid "Level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:console > :metadata"
msgid "Metadata"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:ex_syslogger > :format"
msgid "Format"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:ex_syslogger > :ident"
msgid "Ident"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:ex_syslogger > :level"
msgid "Level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :logger-:ex_syslogger > :metadata"
msgid "Metadata"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types"
msgid "Types"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types > application/activity+json"
msgid "\"application/activity+json\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types > application/jrd+json"
msgid "\"application/jrd+json\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types > application/ld+json"
msgid "\"application/ld+json\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types > application/xml"
msgid "\"application/xml\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :mime > :types > application/xrd+xml"
msgid "\"application/xrd+xml\""
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma > :admin_token"
msgid "Admin token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma > Pleroma.Web.Auth.Authenticator"
msgid "Pleroma.Web.Auth.Authenticator"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :blockers_visible"
msgid "Blockers visible"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :follow_handshake_timeout"
msgid "Follow handshake timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :note_replies_output_limit"
msgid "Note replies output limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :outgoing_blocks"
msgid "Outgoing blocks"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :sign_object_fetches"
msgid "Sign object fetches"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:activitypub > :unfollow_blocked"
msgid "Unfollow blocked"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:assets > :default_mascot"
msgid "Default mascot"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:assets > :default_user_avatar"
msgid "Default user avatar"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:assets > :mascots"
msgid "Mascots"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:auth > :auth_template"
msgid "Auth template"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:auth > :enforce_oauth_admin_scope_usage"
msgid "Enforce OAuth admin scope usage"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:auth > :oauth_consumer_strategies"
msgid "OAuth consumer strategies"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:auth > :oauth_consumer_template"
msgid "OAuth consumer template"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool > :connect_timeout"
msgid "Connect timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool > :connection_acquisition_retries"
msgid "Connection acquisition retries"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool > :connection_acquisition_wait"
msgid "Connection acquisition wait"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool > :max_connections"
msgid "Max connections"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:connections_pool > :reclaim_multiplier"
msgid "Reclaim multiplier"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications > :digest"
msgid "Digest"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications > :digest > :active"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications > :digest > :inactivity_threshold"
msgid "Inactivity threshold"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications > :digest > :interval"
msgid "Interval"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:email_notifications > :digest > :schedule"
msgid "Schedule"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji > :default_manifest"
msgid "Default manifest"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji > :groups"
msgid "Groups"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji > :pack_extensions"
msgid "Pack extensions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji > :shared_pack_cache_seconds_per_file"
msgid "Shared pack cache s/file"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:emoji > :shortcode_globs"
msgid "Shortcode globs"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:features > :improved_hashtag_timeline"
msgid "Improved hashtag timeline"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:feed > :post_title"
msgid "Post title"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:feed > :post_title > :max_length"
msgid "Max length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:feed > :post_title > :omission"
msgid "Omission"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe"
msgid "Pleroma FE"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :alwaysShowSubjectInput"
msgid "Always show subject input"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :background"
msgid "Background"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :collapseMessageWithSubject"
msgid "Collapse message with subject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :greentext"
msgid "Greentext"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideFilteredStatuses"
msgid "Hide Filtered Statuses"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideMutedPosts"
msgid "Hide Muted Posts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hidePostStats"
msgid "Hide post stats"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideSitename"
msgid "Hide Sitename"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :hideUserStats"
msgid "Hide user stats"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logo"
msgid "Logo"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logoMargin"
msgid "Logo margin"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :logoMask"
msgid "Logo mask"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :minimalScopesMode"
msgid "Minimal scopes mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :nsfwCensorImage"
msgid "NSFW Censor Image"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :postContentType"
msgid "Post Content Type"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootLogin"
msgid "Redirect root login"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :redirectRootNoLogin"
msgid "Redirect root no login"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :scopeCopy"
msgid "Scope copy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :showFeaturesPanel"
msgid "Show instance features panel"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :showInstanceSpecificPanel"
msgid "Show instance specific panel"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :sidebarRight"
msgid "Sidebar on Right"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :subjectLineBehavior"
msgid "Subject line behavior"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontend_configurations > :pleroma_fe > :theme"
msgid "Theme"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :admin"
msgid "Admin"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :admin > name"
msgid "Name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :admin > ref"
msgid "Reference"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available"
msgid "Available"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > build_dir"
msgid "Build directory"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > build_url"
msgid "Build URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > custom-http-headers"
msgid "Custom HTTP headers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > git"
msgid "Git Repository URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > name"
msgid "Name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :available > ref"
msgid "Reference"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :primary"
msgid "Primary"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :primary > name"
msgid "Name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:frontends > :primary > ref"
msgid "Reference"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:gopher > :dstport"
msgid "Dstport"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:gopher > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:gopher > :ip"
msgid "IP"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:gopher > :port"
msgid "Port"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :federation"
msgid "Federation"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :federation > :max_connections"
msgid "Max connections"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :federation > :timeout"
msgid "Timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :media"
msgid "Media"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :media > :max_connections"
msgid "Max connections"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :media > :timeout"
msgid "Timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :upload"
msgid "Upload"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :upload > :max_connections"
msgid "Max connections"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:hackney_pools > :upload > :timeout"
msgid "Timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :adapter"
msgid "Adapter"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :adapter > :ssl_options"
msgid "SSL Options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :adapter > :ssl_options > :versions"
msgid "Versions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :proxy_url"
msgid "Proxy URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :send_user_agent"
msgid "Send user agent"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http > :user_agent"
msgid "User agent"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :ct_max_age"
msgid "CT max age"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :referrer_policy"
msgid "Referrer policy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :report_uri"
msgid "Report URI"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :sts"
msgid "STS"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:http_security > :sts_max_age"
msgid "STS max age"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :account_activation_required"
msgid "Account activation required"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :account_approval_required"
msgid "Account approval required"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :account_field_name_length"
msgid "Account field name length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :account_field_value_length"
msgid "Account field value length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :allow_relay"
msgid "Allow relay"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :allowed_post_formats"
msgid "Allowed post formats"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :attachment_links"
msgid "Attachment links"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :autofollowed_nicknames"
msgid "Autofollowed nicknames"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :autofollowing_nicknames"
msgid "Autofollowing nicknames"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :avatar_upload_limit"
msgid "Avatar upload limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :background_upload_limit"
msgid "Background upload limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :banner_upload_limit"
msgid "Banner upload limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :birthday_min_age"
msgid "Birthday min age"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :birthday_required"
msgid "Birthday required"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :cleanup_attachments"
msgid "Cleanup attachments"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :description"
msgid "Description"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :email"
msgid "Admin Email Address"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :extended_nickname_format"
msgid "Extended nickname format"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :external_user_synchronization"
msgid "External user synchronization"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :federating"
msgid "Federating"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :federation_incoming_replies_max_depth"
msgid "Fed. incoming replies max depth"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :federation_reachability_timeout_days"
msgid "Fed. reachability timeout days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :healthcheck"
msgid "Healthcheck"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :instance_thumbnail"
msgid "Instance thumbnail"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :invites_enabled"
msgid "Invites enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :limit"
msgid "Limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :limit_to_local_content"
msgid "Limit to local content"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_account_fields"
msgid "Max account fields"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_endorsed_users"
msgid "Max endorsed users"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_media_attachments"
msgid "Max media attachments"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_pinned_statuses"
msgid "Max pinned statuses"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_remote_account_fields"
msgid "Max remote account fields"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :max_report_comment_size"
msgid "Max report comment size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication"
msgid "Multi factor authentication"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes"
msgid "Backup codes"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :length"
msgid "Length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :backup_codes > :number"
msgid "Number"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp"
msgid "TOTP settings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp > :digits"
msgid "Digits"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :multi_factor_authentication > :totp > :period"
msgid "Period"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :name"
msgid "Name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :notify_email"
msgid "Sender Email Address"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :poll_limits"
msgid "Poll limits"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :poll_limits > :max_expiration"
msgid "Max expiration"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :poll_limits > :max_option_chars"
msgid "Max option chars"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :poll_limits > :max_options"
msgid "Max options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :poll_limits > :min_expiration"
msgid "Min expiration"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :privileged_staff"
msgid "Privileged staff"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :profile_directory"
msgid "Profile directory"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :public"
msgid "Public"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :quarantined_instances"
msgid "Quarantined instances"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :registration_reason_length"
msgid "Registration reason length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :registrations_open"
msgid "Registrations open"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :remote_limit"
msgid "Remote limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :remote_post_retention_days"
msgid "Remote post retention days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :safe_dm_mentions"
msgid "Safe DM mentions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :show_reactions"
msgid "Show reactions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :skip_thread_containment"
msgid "Skip thread containment"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :static_dir"
msgid "Static dir"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :upload_limit"
msgid "Upload limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :user_bio_length"
msgid "User bio length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instance > :user_name_length"
msgid "User name length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:instances_favicons > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :base"
msgid "Base"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :host"
msgid "Host"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :port"
msgid "Port"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :ssl"
msgid "SSL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :sslopts"
msgid "SSL options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :sslopts > :cacertfile"
msgid "Cacertfile"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :sslopts > :verify"
msgid "Verify"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :tls"
msgid "TLS"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :tlsopts"
msgid "TLS options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :tlsopts > :cacertfile"
msgid "Cacertfile"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :tlsopts > :verify"
msgid "Verify"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:ldap > :uid"
msgid "UID"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:majic_pool > :size"
msgid "Size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:manifest > :background_color"
msgid "Background color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:manifest > :icons"
msgid "Icons"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:manifest > :theme_color"
msgid "Theme color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup > :allow_fonts"
msgid "Allow fonts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup > :allow_headings"
msgid "Allow headings"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup > :allow_inline_images"
msgid "Allow inline images"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup > :allow_tables"
msgid "Allow tables"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:markup > :scrub_policy"
msgid "Scrub policy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy > :image_quality"
msgid "Image quality"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy > :min_content_length"
msgid "Min content length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy > :thumbnail_max_height"
msgid "Thumbnail max height"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_preview_proxy > :thumbnail_max_width"
msgid "Thumbnail max width"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :base_url"
msgid "Base URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :invalidation"
msgid "Invalidation"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :invalidation > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :invalidation > :provider"
msgid "Provider"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :proxy_opts"
msgid "Advanced MediaProxy Options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :max_body_length"
msgid "Max body length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :max_read_duration"
msgid "Max read duration"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :proxy_opts > :redirect_on_failure"
msgid "Redirect on failure"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:media_proxy > :whitelist"
msgid "Whitelist"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:modules > :runtime_dir"
msgid "Runtime dir"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf > :policies"
msgid "Policies"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf > :transparency"
msgid "MRF transparency"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf > :transparency_exclusions"
msgid "MRF transparency exclusions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_activity_expiration > :days"
msgid "Days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_follow_bot > :follower_nickname"
msgid "Follower nickname"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hashtag > :federated_timeline_removal"
msgid "Federated timeline removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hashtag > :reject"
msgid "Reject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hashtag > :sensitive"
msgid "Sensitive"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hellthread > :delist_threshold"
msgid "Delist threshold"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_hellthread > :reject_threshold"
msgid "Reject threshold"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_keyword > :federated_timeline_removal"
msgid "Federated timeline removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_keyword > :reject"
msgid "Reject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_keyword > :replace"
msgid "Replace"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_mention > :actors"
msgid "Actors"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_normalize_markup > :scrub_policy"
msgid "Scrub policy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_object_age > :actions"
msgid "Actions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_object_age > :threshold"
msgid "Threshold"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_rejectnonpublic > :allow_direct"
msgid "Allow direct"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_rejectnonpublic > :allow_followersonly"
msgid "Allow followers-only"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :accept"
msgid "Accept"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :avatar_removal"
msgid "Avatar removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :banner_removal"
msgid "Banner removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :federated_timeline_removal"
msgid "Federated timeline removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :followers_only"
msgid "Followers only"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :media_nsfw"
msgid "Media NSFW"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :media_removal"
msgid "Media removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :reject"
msgid "Reject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :reject_deletes"
msgid "Reject deletes"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_simple > :report_removal"
msgid "Report removal"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_steal_emoji > :hosts"
msgid "Hosts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_steal_emoji > :rejected_shortcodes"
msgid "Rejected shortcodes"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_steal_emoji > :size_limit"
msgid "Size limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_subchain > :match_actor"
msgid "Match actor"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_vocabulary > :accept"
msgid "Accept"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:mrf_vocabulary > :reject"
msgid "Reject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:oauth2 > :clean_expired_tokens"
msgid "Clean expired tokens"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:oauth2 > :issue_new_refresh_token"
msgid "Issue new refresh token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:oauth2 > :token_expires_in"
msgid "Token expires in"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :default"
msgid "Default"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :default > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :default > :recv_timeout"
msgid "Recv timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :default > :size"
msgid "Size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :federation"
msgid "Federation"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :federation > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :federation > :recv_timeout"
msgid "Recv timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :federation > :size"
msgid "Size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :media"
msgid "Media"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :media > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :media > :recv_timeout"
msgid "Recv timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :media > :size"
msgid "Size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :upload"
msgid "Upload"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :upload > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :upload > :recv_timeout"
msgid "Recv timeout"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:pools > :upload > :size"
msgid "Size"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:populate_hashtags_table > :fault_rate_allowance"
msgid "Fault rate allowance"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:populate_hashtags_table > :sleep_interval_ms"
msgid "Sleep interval ms"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :app_account_creation"
msgid "App account creation"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :authentication"
msgid "Authentication"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :relation_id_action"
msgid "Relation ID action"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :relations_actions"
msgid "Relations actions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :search"
msgid "Search"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :status_id_action"
msgid "Status ID action"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :statuses_actions"
msgid "Statuses actions"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rate_limit > :timeline"
msgid "Timeline"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities"
msgid "Activities"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities > :local"
msgid "Local"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :activities > :remote"
msgid "Remote"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles"
msgid "Profiles"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles > :local"
msgid "Local"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :profiles > :remote"
msgid "Remote"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines"
msgid "Timelines"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines > :federated"
msgid "Federated"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:restrict_unauthenticated > :timelines > :local"
msgid "Local"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :failure_backoff"
msgid "Failure backoff"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :ignore_hosts"
msgid "Ignore hosts"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :ignore_tld"
msgid "Ignore TLD"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :parsers"
msgid "Parsers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:rich_media > :ttl_setters"
msgid "TTL setters"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:shout > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:shout > :limit"
msgid "Limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:static_fe > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:streamer > :overflow_workers"
msgid "Overflow workers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:streamer > :workers"
msgid "Workers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:uri_schemes > :valid_schemes"
msgid "Valid schemes"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:user > :deny_follow_blocked"
msgid "Deny follow blocked"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:web_cache_ttl > :activity_pub"
msgid "Activity pub"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:web_cache_ttl > :activity_pub_question"
msgid "Activity pub question"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :chat_message"
msgid "Chat message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :chat_message > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :chat_message > :message"
msgid "Message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :chat_message > :sender_nickname"
msgid "Sender nickname"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :direct_message"
msgid "Direct message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :direct_message > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :direct_message > :message"
msgid "Message"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :direct_message > :sender_nickname"
msgid "Sender nickname"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email"
msgid "Email"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email > :html"
msgid "Html"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email > :sender"
msgid "Sender"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email > :subject"
msgid "Subject"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:welcome > :email > :text"
msgid "Text"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-:workers > :retries"
msgid "Retries"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
msgid "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_running"
msgid "Max running"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers"
msgid "Pleroma.Web.RichMedia.Helpers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_running"
msgid "Max running"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-ConcurrentLimiter > Pleroma.Web.RichMedia.Helpers > :max_waiting"
msgid "Max waiting"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :crontab"
msgid "Crontab"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :log"
msgid "Log"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues"
msgid "Queues"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :activity_expiration"
msgid "Activity expiration"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :attachments_cleanup"
msgid "Attachments cleanup"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :background"
msgid "Background"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :backup"
msgid "Backup"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :federator_incoming"
msgid "Federator incoming"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :federator_outgoing"
msgid "Federator outgoing"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :mailer"
msgid "Mailer"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :scheduled_activities"
msgid "Scheduled activities"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :transmogrifier"
msgid "Transmogrifier"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Oban > :queues > :web_push"
msgid "Web push"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha > :method"
msgid "Method"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha > :seconds_valid"
msgid "Seconds valid"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Captcha.Kocaptcha > :endpoint"
msgid "Endpoint"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > :adapter"
msgid "Adapter"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > :enabled"
msgid "Mailer Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:access_key"
msgid "AWS Access Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:region"
msgid "AWS Region"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.AmazonSES-:secret"
msgid "AWS Secret Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Dyn-:api_key"
msgid "Dyn API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Gmail-:access_token"
msgid "GMail API Access Token"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailgun-:api_key"
msgid "Mailgun API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailgun-:domain"
msgid "Domain"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailjet-:api_key"
msgid "MailJet Public API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mailjet-:secret"
msgid "MailJet Private API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Mandrill-:api_key"
msgid "Mandrill API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Postmark-:api_key"
msgid "Postmark API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:auth"
msgid "AUTH Mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:password"
msgid "Password"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:port"
msgid "Port"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:relay"
msgid "Relay"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:retries"
msgid "Retries"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:ssl"
msgid "Use SSL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:tls"
msgid "STARTTLS Mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SMTP-:username"
msgid "Username"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendgrid-:api_key"
msgid "SendGrid API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:cmd_args"
msgid "Cmd args"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:cmd_path"
msgid "Cmd path"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.Sendmail-:qmail"
msgid "Qmail compat mode"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SocketLabs-:api_key"
msgid "SocketLabs API Key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SocketLabs-:server_id"
msgid "Server ID"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SparkPost-:api_key"
msgid "SparkPost API key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.Mailer > Swoosh.Adapters.SparkPost-:endpoint"
msgid "Endpoint"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.NewUsersDigestEmail > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :logo"
msgid "Logo"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling"
msgid "Styling"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :background_color"
msgid "Background color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :content_background_color"
msgid "Content background color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :header_color"
msgid "Header color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :link_color"
msgid "Link color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :text_color"
msgid "Text color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Emails.UserEmail > :styling > :text_muted_color"
msgid "Text muted color"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :class"
msgid "Class"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :extra"
msgid "Extra"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :new_window"
msgid "New window"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :rel"
msgid "Rel"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :strip_prefix"
msgid "Strip prefix"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :truncate"
msgid "Truncate"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Formatter > :validate_tld"
msgid "Validate tld"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :daily_user_limit"
msgid "Daily user limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.ScheduledActivity > :total_user_limit"
msgid "Total user limit"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :base_url"
msgid "Base URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :filename_display_max_length"
msgid "Filename display max length"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :filters"
msgid "Filters"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :link_name"
msgid "Link name"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :proxy_remote"
msgid "Proxy remote"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload > :uploader"
msgid "Uploader"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload.Filter.AnonymizeFilename > :text"
msgid "Text"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Upload.Filter.Mogrify > :args"
msgid "Args"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.Local > :uploads"
msgid "Uploads"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :bucket"
msgid "Bucket"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :bucket_namespace"
msgid "Bucket namespace"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :streaming_enabled"
msgid "Streaming enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Uploaders.S3 > :truncated_namespace"
msgid "Truncated namespace"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User > :email_blacklist"
msgid "Email blacklist"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User > :restricted_nicknames"
msgid "Restricted nicknames"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User.Backup > :limit_days"
msgid "Limit days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.User.Backup > :purge_after_days"
msgid "Purge after days"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.ApiSpec.CastAndValidate > :strict"
msgid "Strict"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :headers"
msgid "Headers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :method"
msgid "Method"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options"
msgid "Options"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Http > :options > :params"
msgid "Params"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :script_path"
msgid "Script path"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.MediaProxy.Invalidation.Script > :url_format"
msgid "URL Format"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Metadata > :providers"
msgid "Providers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Metadata > :unfurl_nsfw"
msgid "Unfurl NSFW"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :headers"
msgid "Headers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :proxies"
msgid "Proxies"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Plugs.RemoteIp > :reserved"
msgid "Reserved"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Web.Preload > :providers"
msgid "Providers"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :pleroma-Pleroma.Workers.PurgeExpiredActivity > :min_lifetime"
msgid "Min lifetime"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :auth"
msgid "Auth"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :enabled"
msgid "Enabled"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :format"
msgid "Format"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :ip_whitelist"
msgid "IP Whitelist"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :prometheus-Pleroma.Web.Endpoint.MetricsExporter > :path"
msgid "Path"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :quack > :level"
msgid "Level"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :quack > :meta"
msgid "Meta"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :quack > :webhook_url"
msgid "Webhook URL"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :web_push_encryption-:vapid_details > :private_key"
msgid "Private key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :web_push_encryption-:vapid_details > :public_key"
msgid "Public key"
msgstr ""
#: lib/pleroma/docs/translator.ex:5
#, elixir-autogen, elixir-format
msgctxt "config label at :web_push_encryption-:vapid_details > :subject"
msgid "Subject"
msgstr ""
diff --git a/priv/gettext/fr/LC_MESSAGES/errors.po b/priv/gettext/fr/LC_MESSAGES/errors.po
index 406f98de9..20636cee3 100644
--- a/priv/gettext/fr/LC_MESSAGES/errors.po
+++ b/priv/gettext/fr/LC_MESSAGES/errors.po
@@ -1,467 +1,466 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
-"PO-Revision-Date: 2020-05-12 15:52+0000\n"
-"Last-Translator: Haelwenn (lanodan) Monnier "
-"<contact+translate.pleroma.social@hacktivis.me>\n"
+"PO-Revision-Date: 2025-12-27 07:08+0000\n"
+"Last-Translator: Codimp <contact@lithio.fr>\n"
"Language-Team: French <https://translate.pleroma.social/projects/pleroma/"
-"pleroma/fr/>\n"
+"pleroma-backend-domain-errors/fr/>\n"
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
-"Plural-Forms: nplurals=2; plural=n > 1;\n"
-"X-Generator: Weblate 4.0.4\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 4.13.1\n"
msgid "can't be blank"
msgstr "ne peut être vide"
## grammatical gender…
msgid "has already been taken"
msgstr "a déjà été pris"
msgid "is invalid"
msgstr "est invalide"
msgid "has invalid format"
msgstr "a un format invalide"
msgid "has an invalid entry"
msgstr "a une entrée invalide"
## grammatical gender…
msgid "is reserved"
msgstr "est réservé"
msgid "does not match confirmation"
msgstr "ne correspondent pas"
msgid "is still associated with this entry"
msgstr "est toujours associé à cette entrée"
msgid "are still associated with this entry"
msgstr "sont toujours associés à cette entrée"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "devrait avoir %{count} charactère"
msgstr[1] "devrait avoir %{count} charactères"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "devrait avoir %{count} objet"
msgstr[1] "devrait avoir %{count} objets"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "devrait avoir au moins %{count} charactère"
msgstr[1] "devrait avoir au moins %{count} charactères"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "devrait avoir au moins %{count} objet"
msgstr[1] "devrait avoir au moins %{count} objets"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "devrait avoir au plus %{count} charactère"
msgstr[1] "devrait avoir au plus %{count} charactères"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "devrait avoir au plus %{count} objet"
msgstr[1] "devrait avoir au plus %{count} objets"
msgid "must be less than %{number}"
msgstr "doit être inférieur à %{number}"
msgid "must be greater than %{number}"
msgstr "doit être supérieur à %{number}"
msgid "must be less than or equal to %{number}"
msgstr "doit être inférieur ou égal à %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "doit être supérieur ou égal à %{number}"
msgid "must be equal to %{number}"
msgstr "doit égal à %{number}"
#: lib/pleroma/web/common_api/common_api.ex:381
#, elixir-format
msgid "Account not found"
msgstr "Compte non trouvé"
#: lib/pleroma/web/common_api/common_api.ex:153
#, elixir-format
msgid "Already voted"
msgstr "A déjà voté"
#: lib/pleroma/web/oauth/oauth_controller.ex:263
#, elixir-format
msgid "Bad request"
msgstr "Requête Invalide"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:254
#, elixir-format
msgid "Can't delete object"
msgstr "Ne peut supprimer cet objet"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:569
#, elixir-format
msgid "Can't delete this post"
msgstr "Ne peut supprimer ce message"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1731
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1737
#, elixir-format
msgid "Can't display this activity"
msgstr "Ne peut afficher cette activitée"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:195
#, elixir-format
msgid "Can't find user"
msgstr "Compte non trouvé"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1148
#, elixir-format
msgid "Can't get favorites"
msgstr "Favoris non trouvables"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:263
#, elixir-format
msgid "Can't like object"
msgstr "Ne peut aimer cet objet"
#: lib/pleroma/web/common_api/utils.ex:518
#, elixir-format
msgid "Cannot post an empty status without attachments"
msgstr "Ne peut envoyer un status vide sans attachements"
#: lib/pleroma/web/common_api/utils.ex:461
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
msgstr "Le commentaire ne doit faire plus de %{max_size} charactères"
#: lib/pleroma/web/admin_api/config.ex:63
#, elixir-format
msgid "Config with params %{params} not found"
msgstr "Configuration avec les paramètres %{params} non trouvée"
#: lib/pleroma/web/common_api/common_api.ex:78
#, elixir-format
msgid "Could not delete"
msgstr "Échec de la suppression"
#: lib/pleroma/web/common_api/common_api.ex:110
#, elixir-format
msgid "Could not favorite"
msgstr "Échec de mise en favoris"
#: lib/pleroma/web/common_api/common_api.ex:310
#, elixir-format
msgid "Could not pin"
msgstr "Échec de l'épinglage"
#: lib/pleroma/web/common_api/common_api.ex:89
#, elixir-format
msgid "Could not repeat"
msgstr "Échec de création la répétition"
#: lib/pleroma/web/common_api/common_api.ex:120
#, elixir-format
msgid "Could not unfavorite"
msgstr "Échec de suppression des favoris"
#: lib/pleroma/web/common_api/common_api.ex:327
#, elixir-format
msgid "Could not unpin"
msgstr "Échec du dépinglage"
#: lib/pleroma/web/common_api/common_api.ex:99
#, elixir-format
msgid "Could not unrepeat"
msgstr "Échec de suppression de la répétition"
#: lib/pleroma/web/common_api/common_api.ex:392
#, elixir-format
msgid "Could not update state"
msgstr "Échec de la mise à jour du status"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1271
#, elixir-format
msgid "Error."
msgstr "Erreur."
#: lib/pleroma/captcha/kocaptcha.ex:36
#, elixir-format
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA invalide"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1700
#: lib/pleroma/web/oauth/oauth_controller.ex:465
#, elixir-format
msgid "Invalid credentials"
msgstr "Paramètres d'authentification invalides"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:20
#, elixir-format
msgid "Invalid credentials."
msgstr "Paramètres d'authentification invalides."
#: lib/pleroma/web/common_api/common_api.ex:154
#, elixir-format
msgid "Invalid indices"
msgstr "Indices invalides"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:411
#, elixir-format
msgid "Invalid parameters"
msgstr "Paramètres invalides"
#: lib/pleroma/web/common_api/utils.ex:377
#, elixir-format
msgid "Invalid password."
msgstr "Mot de passe invalide."
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:163
#, elixir-format
msgid "Invalid request"
msgstr "Requête invalide"
#: lib/pleroma/captcha/kocaptcha.ex:16
#, elixir-format
msgid "Kocaptcha service unavailable"
msgstr "Service Kocaptcha non disponible"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1696
#, elixir-format
msgid "Missing parameters"
msgstr "Paramètres manquants"
#: lib/pleroma/web/common_api/utils.ex:496
#, elixir-format
msgid "No such conversation"
msgstr "Conversation inconnue"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:163
#: lib/pleroma/web/admin_api/admin_api_controller.ex:206
#, elixir-format
msgid "No such permission_group"
msgstr "Groupe de permission inconnu"
#: lib/pleroma/plugs/uploaded_media.ex:69
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:311
#: lib/pleroma/web/admin_api/admin_api_controller.ex:399
#: lib/pleroma/web/mastodon_api/subscription_controller.ex:63
#: lib/pleroma/web/ostatus/ostatus_controller.ex:248
#, elixir-format
msgid "Not found"
msgstr "Non Trouvé"
#: lib/pleroma/web/common_api/common_api.ex:152
#, elixir-format
msgid "Poll's author can't vote"
msgstr "L'auteur·rice d'un sondage ne peut voter"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:443
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:444
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:473
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:476
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1180
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1564
#, elixir-format
msgid "Record not found"
msgstr "Enregistrement non trouvé"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:417
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1570
#: lib/pleroma/web/mastodon_api/subscription_controller.ex:69
#: lib/pleroma/web/ostatus/ostatus_controller.ex:252
#, elixir-format
msgid "Something went wrong"
msgstr "Erreur inconnue"
#: lib/pleroma/web/common_api/common_api.ex:253
#, elixir-format
msgid "The message visibility must be direct"
msgstr "La visibilitée du message doit être « direct »"
#: lib/pleroma/web/common_api/utils.ex:521
#, elixir-format
msgid "The status is over the character limit"
msgstr "Le status est au-delà de la limite de charactères"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:27
#, elixir-format
msgid "This resource requires authentication."
msgstr "Cette resource nécessite une authentification."
#: lib/pleroma/plugs/rate_limiter.ex:89
#, elixir-format
msgid "Throttled"
msgstr "Limité"
#: lib/pleroma/web/common_api/common_api.ex:155
#, elixir-format
msgid "Too many choices"
msgstr "Trop de choix"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:268
#, elixir-format
msgid "Unhandled activity type"
msgstr "Type d'activitée non-gérée"
#: lib/pleroma/plugs/user_is_admin_plug.ex:20
#, elixir-format
msgid "User is not admin."
msgstr "Le compte n'est pas admin."
#: lib/pleroma/web/common_api/common_api.ex:380
#, elixir-format
msgid "Valid `account_id` required"
msgstr "Un `account_id` valide est requis"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:185
#, elixir-format
msgid "You can't revoke your own admin status."
msgstr "Vous ne pouvez révoquer votre propre status d'admin."
#: lib/pleroma/web/oauth/oauth_controller.ex:216
#, elixir-format
msgid "Your account is currently disabled"
msgstr "Votre compte est actuellement désactivé"
#: lib/pleroma/web/oauth/oauth_controller.ex:158
#: lib/pleroma/web/oauth/oauth_controller.ex:213
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
msgstr "Une confirmation de l'addresse de couriel est requise pour l'authentification"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:221
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr "Ne peut lire la boite de réception de %{nickname} en tant que %{as_nickname}"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:297
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr "Ne peut poster dans la boite d'émission de %{nickname} en tant que %{as_nickname}"
#: lib/pleroma/web/common_api/common_api.ex:335
#, elixir-format
msgid "conversation is already muted"
-msgstr "la conversation est déjà baillonée"
+msgstr "la conversation est déjà silenciée"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:192
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:317
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1196
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:1247
#, elixir-format
msgid "error"
msgstr "erreur"
#: lib/pleroma/web/mastodon_api/mastodon_api_controller.ex:789
#, elixir-format
msgid "mascots can only be images"
msgstr "les mascottes ne peuvent être que des images"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:34
#, elixir-format
msgid "not found"
msgstr "non trouvé"
#: lib/pleroma/web/oauth/oauth_controller.ex:298
#, elixir-format
msgid "Bad OAuth request."
msgstr "Requête OAuth invalide."
#: lib/pleroma/captcha/captcha.ex:92
#, elixir-format
msgid "CAPTCHA already used"
msgstr "CAPTCHA déjà utilisé"
#: lib/pleroma/captcha/captcha.ex:89
#, elixir-format
msgid "CAPTCHA expired"
msgstr "CAPTCHA expiré"
#: lib/pleroma/plugs/uploaded_media.ex:50
#, elixir-format
msgid "Failed"
msgstr "Échec"
#: lib/pleroma/web/oauth/oauth_controller.ex:314
#, elixir-format
msgid "Failed to authenticate: %{message}."
msgstr "Échec de l'authentification : %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:345
#, elixir-format
msgid "Failed to set up user account."
msgstr "Échec de création de votre compte."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:37
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
msgstr "Permissions insuffisantes : %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:89
#, elixir-format
msgid "Internal Error"
msgstr "Erreur interne"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
-msgstr "Nom d'utilisateur/mot de passe invalide"
+msgstr "Nom d'utilisateur·ice/mot de passe invalide"
#: lib/pleroma/captcha/captcha.ex:107
#, elixir-format
msgid "Invalid answer data"
msgstr "Réponse invalide"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:204
#, elixir-format
msgid "Nodeinfo schema version not handled"
msgstr "Version du schéma nodeinfo non géré"
#: lib/pleroma/web/oauth/oauth_controller.ex:145
#, elixir-format
msgid "This action is outside the authorized scopes"
msgstr "Cette action est en dehors des authorisations" # "scopes"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
msgstr "Erreur inconnue, veuillez vérifier les détails et réessayer."
#: lib/pleroma/web/oauth/oauth_controller.ex:93
#: lib/pleroma/web/oauth/oauth_controller.ex:131
#, elixir-format
msgid "Unlisted redirect_uri."
msgstr "redirect_uri non listé."
#: lib/pleroma/web/oauth/oauth_controller.ex:294
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
msgstr "Fournisseur OAuth non supporté : %{provider}."
#: lib/pleroma/uploaders/uploader.ex:71
#, elixir-format
msgid "Uploader callback timeout"
msgstr "Temps d'attente du téléverseur écoulé"
## msgstr "Attente écoulée"
#: lib/pleroma/web/uploader_controller.ex:11
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
msgstr "requête invalide"
diff --git a/priv/gettext/id/LC_MESSAGES/errors.po b/priv/gettext/id/LC_MESSAGES/errors.po
index bc98273be..5bb9d56c6 100644
--- a/priv/gettext/id/LC_MESSAGES/errors.po
+++ b/priv/gettext/id/LC_MESSAGES/errors.po
@@ -1,580 +1,580 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-08-15 15:40+0000\n"
-"PO-Revision-Date: 2021-09-03 06:45+0000\n"
-"Last-Translator: @liimee <alt3753.7@gmail.com>\n"
+"PO-Revision-Date: 2025-07-31 05:58+0000\n"
+"Last-Translator: Neko Nekowazarashi <kodra@nekoweb.my.id>\n"
"Language-Team: Indonesian <https://translate.pleroma.social/projects/pleroma/"
-"pleroma/id/>\n"
+"pleroma-backend-domain-errors/id/>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Weblate 4.6.2\n"
+"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr "tidak boleh kosong"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr "sudah diambil"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr "tidak valid"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr "memiliki format yang tidak valid"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
-msgstr ""
+msgstr "ada yang tidak valid"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
-msgstr ""
+msgstr "tidak sama dengan konfirmasi"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
-msgstr[0] "harus memiliki %{count} karakter"
+msgstr[0] "harus ada %{count} karakter"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
-msgstr[0] "harus memiliki %{count} item"
+msgstr[0] "harus ada %{count} item"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
-msgstr[0] "harus memiliki sekurang-kurangnya %{count} karakter"
+msgstr[0] "harus ada sekurang-kurangnya %{count} karakter"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
-msgstr[0] "harus memiliki sekurang-kurangnya %{count} item"
+msgstr[0] "harus ada sekurang-kurangnya %{count} item"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr "harus kurang dari %{number}"
msgid "must be greater than %{number}"
msgstr "harus lebih dari %{number}"
msgid "must be less than or equal to %{number}"
msgstr "harus kurang dari atau sama dengan %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "harus lebih dari atau sama dengan %{number}"
msgid "must be equal to %{number}"
msgstr "harus sama dengan %{number}"
#: lib/pleroma/web/common_api/common_api.ex:505
#, elixir-format
msgid "Account not found"
msgstr "Akun tidak ditemukan"
#: lib/pleroma/web/common_api/common_api.ex:339
#, elixir-format
msgid "Already voted"
msgstr "Sudah memilih"
#: lib/pleroma/web/oauth/oauth_controller.ex:359
#, elixir-format
msgid "Bad request"
-msgstr "Permintaan buruk (bad request)"
+msgstr "Permintaan buruk"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426
#, elixir-format
msgid "Can't delete object"
msgstr "Tidak dapat menghapus objek"
#: lib/pleroma/web/controller_helper.ex:105
#: lib/pleroma/web/controller_helper.ex:111
#, elixir-format
msgid "Can't display this activity"
msgstr "Tidak dapat menampilkan aktivitas ini"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285
#, elixir-format
msgid "Can't find user"
msgstr "Tidak dapat mencari pengguna"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61
#, elixir-format
msgid "Can't get favorites"
-msgstr "Tidak dapat mendapatkan favorit"
+msgstr "Tidak dapat mengambil favorit"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438
#, elixir-format
msgid "Can't like object"
msgstr "Tidak dapat menyukai objek"
#: lib/pleroma/web/common_api/utils.ex:563
#, elixir-format
msgid "Cannot post an empty status without attachments"
-msgstr "Tidak dapat memposting status kosong tanpa lampiran"
+msgstr "Tidak dapat mempos status kosong tanpa lampiran"
#: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
msgstr "Komentar tidak boleh lebih dari %{max_size} karakter"
#: lib/pleroma/config/config_db.ex:191
#, elixir-format
msgid "Config with params %{params} not found"
msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:181
#: lib/pleroma/web/common_api/common_api.ex:185
#, elixir-format
msgid "Could not delete"
msgstr "Tidak dapat menghapus"
#: lib/pleroma/web/common_api/common_api.ex:231
#, elixir-format
msgid "Could not favorite"
msgstr "Tidak dapat memfavoritkan"
#: lib/pleroma/web/common_api/common_api.ex:453
#, elixir-format
msgid "Could not pin"
msgstr "Tidak dapat menyematkan"
#: lib/pleroma/web/common_api/common_api.ex:278
#, elixir-format
msgid "Could not unfavorite"
msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:463
#, elixir-format
msgid "Could not unpin"
msgstr "Tidak dapat berhenti menyematkan"
#: lib/pleroma/web/common_api/common_api.ex:216
#, elixir-format
msgid "Could not unrepeat"
msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:512
#: lib/pleroma/web/common_api/common_api.ex:521
#, elixir-format
msgid "Could not update state"
msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207
#, elixir-format
msgid "Error."
msgstr "Kesalahan."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format
msgid "Invalid CAPTCHA"
msgstr "CAPTCHA tidak valid"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:568
#, elixir-format
msgid "Invalid credentials"
-msgstr "Kredensian tidak valid"
+msgstr "Kredensial tidak valid"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format
msgid "Invalid credentials."
msgstr "Kredensial tidak valid."
#: lib/pleroma/web/common_api/common_api.ex:355
#, elixir-format
msgid "Invalid indices"
msgstr ""
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29
#, elixir-format
msgid "Invalid parameters"
msgstr "Parameter tidak valid"
#: lib/pleroma/web/common_api/utils.ex:414
#, elixir-format
msgid "Invalid password."
msgstr "Kata sandi tidak valid."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#, elixir-format
msgid "Invalid request"
msgstr "Permintaan tidak valid"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
msgstr "Layanan Kocaptcha tidak tersedia"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#, elixir-format
msgid "Missing parameters"
msgstr "Parameter kurang"
#: lib/pleroma/web/common_api/utils.ex:547
#, elixir-format
msgid "No such conversation"
msgstr "Tidak ada percakapan ini"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
#, elixir-format
msgid "No such permission_group"
msgstr ""
#: lib/pleroma/plugs/uploaded_media.ex:84
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:486 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:11
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format
msgid "Not found"
msgstr "Tidak ditemukan"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
msgstr "Pembuat japat tidak boleh memilih"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:50 lib/pleroma/web/mastodon_api/controllers/status_controller.ex:306
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format
msgid "Record not found"
msgstr ""
#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:35
#: lib/pleroma/web/feed/user_controller.ex:77 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:36
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#, elixir-format
msgid "Something went wrong"
-msgstr "Sesuatu yang salah terjadi"
+msgstr "Ada sesuatu yang salah"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
-msgstr "Visibilitas pesan harus langsung"
+msgstr "Ketampakan pesan harus langsung"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
msgstr "Status lebih dari batas karakter"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
-msgstr ""
+msgstr "Autentikasi diperlukan untuk hal ini."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
msgid "Throttled"
msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
msgstr "Terlalu banyak pilihan"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
msgid "Unhandled activity type"
msgstr ""
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485
#, elixir-format
msgid "You can't revoke your own admin status."
-msgstr ""
+msgstr "Anda tidak dapat mencabut status admin Anda."
#: lib/pleroma/web/oauth/oauth_controller.ex:221
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
msgstr "Akun Anda saat ini dinonaktifkan"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
msgstr "percakapan sudah dibisukan"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
msgstr "kesalahan"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
msgid "mascots can only be images"
msgstr "maskot hanya dapat berbentuk gambar"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
msgstr "tidak ditemukan"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
msgid "Bad OAuth request."
msgstr "Permintaan OAuth buruk."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format
msgid "CAPTCHA already used"
msgstr "CAPTCHA sudah digunakan"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format
msgid "CAPTCHA expired"
msgstr "CAPTCHA kedaluwarsa"
#: lib/pleroma/plugs/uploaded_media.ex:57
#, elixir-format
msgid "Failed"
msgstr "Gagal"
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
-msgstr "Gagal mengotentikasi: %{message}."
+msgstr "Gagal mengautentikasi: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
msgstr "Gagal menyiapkan akun pengguna."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
-msgstr ""
+msgstr "Izin tidak cukup: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
msgstr "Kesalahan Internal"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
msgstr "Nama Pengguna/Kata Sandi Tidak Valid"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
msgid "Invalid answer data"
msgstr ""
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33
#, elixir-format
msgid "Nodeinfo schema version not handled"
msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:172
#, elixir-format
msgid "This action is outside the authorized scopes"
-msgstr ""
+msgstr "Tindakan ini diluar jangkauan yang terotorisasi"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
-msgstr "Kesalahan tidak dikenal, harap periksa keterangannya dan coba lagi."
+msgstr "Kesalahan tidak dikenal, harap periksa detailnya dan coba lagi."
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
#, elixir-format
msgid "Unlisted redirect_uri."
msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:390
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
msgstr "Penyedia OAuth tidak didukung: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format
msgid "Uploader callback timeout"
msgstr ""
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
-msgstr "permintaan buruk (bad request)"
+msgstr "permintaan buruk"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
msgstr "Kesalahan CAPTCHA"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
msgid "Could not add reaction emoji"
msgstr "Tidak dapat menambahkan emoji reaksi"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
msgstr "Tidak dapat menghapus reaksi emoji"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr "CAPTCHA Tidak Valid (Parameter kurang: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
-msgstr ""
+msgstr "Daftar tidak ditemukan"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
msgid "Missing parameter: %{name}"
msgstr "Parameter kurang: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:210
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
-msgstr ""
+msgstr "Diperlukan atur ulang kata sandi"
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/config_controller.ex:6 lib/pleroma/web/admin_api/controllers/fallback_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/invite_controller.ex:6 lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex:6 lib/pleroma/web/admin_api/controllers/relay_controller.ex:6
#: lib/pleroma/web/admin_api/controllers/report_controller.ex:6 lib/pleroma/web/admin_api/controllers/status_controller.ex:6
#: lib/pleroma/web/controller_helper.ex:6 lib/pleroma/web/embed_controller.ex:6
#: lib/pleroma/web/fallback_redirect_controller.ex:6 lib/pleroma/web/feed/tag_controller.ex:6
#: lib/pleroma/web/feed/user_controller.ex:6 lib/pleroma/web/mailer/subscription_controller.ex:2
#: lib/pleroma/web/masto_fe_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/account_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/app_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/auth_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/filter_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/instance_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/list_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/marker_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex:14
#: lib/pleroma/web/mastodon_api/controllers/media_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/notification_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/report_controller.ex:8
#: lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/search_controller.ex:6
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:7
#: lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex:6 lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:6
#: lib/pleroma/web/media_proxy/media_proxy_controller.ex:6 lib/pleroma/web/mongooseim/mongoose_im_controller.ex:6
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:6 lib/pleroma/web/oauth/fallback_controller.ex:6
#: lib/pleroma/web/oauth/mfa_controller.ex:10 lib/pleroma/web/oauth/oauth_controller.ex:6
#: lib/pleroma/web/ostatus/ostatus_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/account_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/chat_controller.ex:5 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:2 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex:6
#: lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex:7 lib/pleroma/web/static_fe/static_fe_controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/password_controller.ex:10 lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex:6
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:6 lib/pleroma/web/twitter_api/twitter_api_controller.ex:6
#: lib/pleroma/web/uploader_controller.ex:6 lib/pleroma/web/web_finger/web_finger_controller.ex:6
#, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
-msgstr "Otentikasi dua-faktor diaktifkan, Anda harus menggunakan token akses."
+msgstr "Autentikasi dua langkah diaktifkan, Anda harus menggunakan token akses."
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
msgid "Unexpected error occurred while adding file to pack."
msgstr ""
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138
#, elixir-format
msgid "Unexpected error occurred while creating pack."
msgstr ""
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278
#, elixir-format
msgid "Unexpected error occurred while removing file from pack."
msgstr ""
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250
#, elixir-format
msgid "Unexpected error occurred while updating file in pack."
msgstr ""
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179
#, elixir-format
msgid "Unexpected error occurred while updating pack metadata."
msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
-msgstr ""
+msgstr "Langganan push web dinonaktifkan untuk peladen Pleroma ini"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
msgid "You can't revoke your own admin/moderator status."
msgstr "Anda tidak bisa mencabut status admin/moderator Anda sendiri."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
-msgstr ""
+msgstr "Otorisasi diperlukan untuk tampilan linimasa"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
msgstr "Akses ditolak"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
-msgstr ""
+msgstr "API ini memerlukan pengguna terautentikasi"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
msgstr "Pengguna bukan seorang admin."
diff --git a/priv/gettext/id/LC_MESSAGES/posix_errors.po b/priv/gettext/id/LC_MESSAGES/posix_errors.po
new file mode 100644
index 000000000..f5ea93285
--- /dev/null
+++ b/priv/gettext/id/LC_MESSAGES/posix_errors.po
@@ -0,0 +1,163 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-08-11 18:40+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: id\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Translate Toolkit 3.7.2\n"
+
+## This file is a PO Template file.
+##
+## `msgid`s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run `mix gettext.extract` to bring this file up to
+## date. Leave `msgstr`s empty as changing them here as no
+## effect: edit them in PO (`.po`) files instead.
+msgid "eperm"
+msgstr ""
+
+msgid "eacces"
+msgstr ""
+
+msgid "eagain"
+msgstr ""
+
+msgid "ebadf"
+msgstr ""
+
+msgid "ebadmsg"
+msgstr ""
+
+msgid "ebusy"
+msgstr ""
+
+msgid "edeadlk"
+msgstr ""
+
+msgid "edeadlock"
+msgstr ""
+
+msgid "edquot"
+msgstr ""
+
+msgid "eexist"
+msgstr ""
+
+msgid "efault"
+msgstr ""
+
+msgid "efbig"
+msgstr ""
+
+msgid "eftype"
+msgstr ""
+
+msgid "eintr"
+msgstr ""
+
+msgid "einval"
+msgstr ""
+
+msgid "eio"
+msgstr ""
+
+msgid "eisdir"
+msgstr ""
+
+msgid "eloop"
+msgstr ""
+
+msgid "emfile"
+msgstr ""
+
+msgid "emlink"
+msgstr ""
+
+msgid "emultihop"
+msgstr ""
+
+msgid "enametoolong"
+msgstr ""
+
+msgid "enfile"
+msgstr ""
+
+msgid "enobufs"
+msgstr ""
+
+msgid "enodev"
+msgstr ""
+
+msgid "enolck"
+msgstr ""
+
+msgid "enolink"
+msgstr ""
+
+msgid "enoent"
+msgstr ""
+
+msgid "enomem"
+msgstr ""
+
+msgid "enospc"
+msgstr ""
+
+msgid "enosr"
+msgstr ""
+
+msgid "enostr"
+msgstr ""
+
+msgid "enosys"
+msgstr ""
+
+msgid "enotblk"
+msgstr ""
+
+msgid "enotdir"
+msgstr ""
+
+msgid "enotsup"
+msgstr ""
+
+msgid "enxio"
+msgstr ""
+
+msgid "eopnotsupp"
+msgstr ""
+
+msgid "eoverflow"
+msgstr ""
+
+msgid "epipe"
+msgstr ""
+
+msgid "erange"
+msgstr ""
+
+msgid "erofs"
+msgstr ""
+
+msgid "espipe"
+msgstr ""
+
+msgid "esrch"
+msgstr ""
+
+msgid "estale"
+msgstr ""
+
+msgid "etxtbsy"
+msgstr ""
+
+msgid "exdev"
+msgstr ""
diff --git a/priv/gettext/id/LC_MESSAGES/static_pages.po b/priv/gettext/id/LC_MESSAGES/static_pages.po
new file mode 100644
index 000000000..e2565888a
--- /dev/null
+++ b/priv/gettext/id/LC_MESSAGES/static_pages.po
@@ -0,0 +1,574 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-08-11 18:40+0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Language: id\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Translate Toolkit 3.7.2\n"
+
+## This file is a PO Template file.
+##
+## "msgid"s here are often extracted from source code.
+## Add new translations manually only if they're dynamic
+## translations that can't be statically extracted.
+##
+## Run "mix gettext.extract" to bring this file up to
+## date. Leave "msgstr"s empty as changing them here as no
+## effect: edit them in PO (.po) files instead.
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error fetching user"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header"
+msgid "Remote follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for auth code entry"
+msgid "Authentication code"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for password entry"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for username entry"
+msgid "Username"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for login"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for mfa"
+msgid "Authorize"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error following account"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header, need login"
+msgid "Log in to follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow mfa header"
+msgid "Two-factor authentication"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow success"
+msgid "Account followed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
+#, elixir-autogen, elixir-format
+msgctxt "placeholder text for account id"
+msgid "Your account ID, e.g. lain@quitter.se"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "remote follow authorization button for following with a remote account"
+msgid "Follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error"
+msgid "Error: %{error}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "remote follow header"
+msgid "Remotely follow %{nickname}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "password reset button"
+msgid "Reset"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "password reset failed homepage link"
+msgid "Homepage"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset failed message"
+msgid "Password reset failed"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "password reset form confirm password prompt"
+msgid "Confirmation"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "password reset form password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset invalid token message"
+msgid "Invalid Token"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "password reset successful homepage link"
+msgid "Homepage"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "password reset successful message"
+msgid "Password changed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:12
+#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "tag feed description"
+msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorization exists page title"
+msgid "Authorization exists"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize approve button"
+msgid "Approve"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize cancel button"
+msgid "Cancel"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorize message"
+msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth authorized page title"
+msgid "Successfully authorized"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "oauth external provider page title"
+msgid "Sign in with external provider"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
+#, elixir-autogen, elixir-format
+msgctxt "oauth external provider sign in button"
+msgid "Sign in with %{strategy}"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
+#, elixir-autogen, elixir-format
+msgctxt "oauth login button"
+msgid "Log In"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
+#, elixir-autogen, elixir-format
+msgctxt "oauth login password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
+#, elixir-autogen, elixir-format
+msgctxt "oauth login username prompt"
+msgid "Username"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
+#, elixir-autogen, elixir-format
+msgctxt "oauth register nickname prompt"
+msgid "Pleroma Handle"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
+#, elixir-autogen, elixir-format
+msgctxt "oauth register nickname unchangeable warning"
+msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page email prompt"
+msgid "Email"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page fill form prompt"
+msgid "If you'd like to register a new account, please provide the details below."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login button"
+msgid "Proceed as existing user"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login password prompt"
+msgid "Password"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login prompt"
+msgid "Alternatively, sign in to connect to existing account."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page login username prompt"
+msgid "Name or email"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page nickname prompt"
+msgid "Nickname"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page register button"
+msgid "Proceed as new user"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page title"
+msgid "Registration Details"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
+#, elixir-autogen, elixir-format
+msgctxt "oauth register page title"
+msgid "This is the first time you visit! Please enter your Pleroma handle."
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "oauth scopes message"
+msgid "The following permissions will be granted"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
+#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "oauth token code message"
+msgid "Token code is <br>%{token}"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth code prompt"
+msgid "Authentication code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth page title"
+msgid "Two-factor authentication"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth page use recovery code link"
+msgid "Enter a two-factor recovery code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
+#, elixir-autogen, elixir-format
+msgctxt "mfa auth verify code button"
+msgid "Verify"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover page title"
+msgid "Two-factor recovery"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover recovery code prompt"
+msgid "Recovery code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover use 2fa code link"
+msgid "Enter a two-factor code"
+msgstr ""
+
+#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
+#, elixir-autogen, elixir-format
+msgctxt "mfa recover verify recovery code button"
+msgid "Verify"
+msgstr ""
+
+#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "static fe profile page remote follow button"
+msgid "Remote follow"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:163
+#, elixir-autogen, elixir-format
+msgctxt "digest email header line"
+msgid "Hey %{nickname}, here is what you've missed!"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:544
+#, elixir-autogen, elixir-format
+msgctxt "digest email receiver address"
+msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:538
+#, elixir-autogen, elixir-format
+msgctxt "digest email sending reason"
+msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+#, elixir-autogen, elixir-format
+msgctxt "digest email unsubscribe action"
+msgid "To unsubscribe, please go %{here}."
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:547
+#, elixir-autogen, elixir-format
+msgctxt "digest email unsubscribe action link text"
+msgid "here"
+msgstr ""
+
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "mailer unsubscribe failed message"
+msgid "UNSUBSCRIBE FAILURE"
+msgstr ""
+
+#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
+#, elixir-autogen, elixir-format
+msgctxt "mailer unsubscribe successful message"
+msgid "UNSUBSCRIBE SUCCESSFUL"
+msgstr ""
+
+#: lib/pleroma/web/templates/email/digest.html.eex:385
+#, elixir-format
+msgctxt "new followers count header"
+msgid "%{count} New Follower"
+msgid_plural "%{count} New Followers"
+msgstr[0] ""
+msgstr[1] ""
+
+#: lib/pleroma/emails/user_email.ex:356
+#, elixir-autogen, elixir-format
+msgctxt "account archive email body - self-requested"
+msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:384
+#, elixir-autogen, elixir-format
+msgctxt "account archive email subject"
+msgid "Your account archive is ready"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:188
+#, elixir-autogen, elixir-format
+msgctxt "approval pending email body"
+msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:202
+#, elixir-autogen, elixir-format
+msgctxt "approval pending email subject"
+msgid "Your account is awaiting approval"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:158
+#, elixir-autogen, elixir-format
+msgctxt "confirmation email body"
+msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:174
+#, elixir-autogen, elixir-format
+msgctxt "confirmation email subject"
+msgid "%{instance_name} account confirmation"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:310
+#, elixir-autogen, elixir-format
+msgctxt "digest email subject"
+msgid "Your digest from %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:81
+#, elixir-autogen, elixir-format
+msgctxt "password reset email body"
+msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:98
+#, elixir-autogen, elixir-format
+msgctxt "password reset email subject"
+msgid "Password reset"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:215
+#, elixir-autogen, elixir-format
+msgctxt "successful registration email body"
+msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:231
+#, elixir-autogen, elixir-format
+msgctxt "successful registration email subject"
+msgid "Account registered on %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:119
+#, elixir-autogen, elixir-format
+msgctxt "user invitation email body"
+msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:136
+#, elixir-autogen, elixir-format
+msgctxt "user invitation email subject"
+msgid "Invitation to %{instance_name}"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:53
+#, elixir-autogen, elixir-format
+msgctxt "welcome email html body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:41
+#, elixir-autogen, elixir-format
+msgctxt "welcome email subject"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:65
+#, elixir-autogen, elixir-format
+msgctxt "welcome email text body"
+msgid "Welcome to %{instance_name}!"
+msgstr ""
+
+#: lib/pleroma/emails/user_email.ex:368
+#, elixir-autogen, elixir-format
+msgctxt "account archive email body - admin requested"
+msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error message - unknown error"
+msgid "Something went wrong."
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
+#, elixir-autogen, elixir-format
+msgctxt "remote follow error message - user not found"
+msgid "Could not find user"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
+#, elixir-autogen, elixir-format
+msgctxt "status interact authorization button"
+msgid "Interact"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
+#, elixir-autogen, elixir-format
+msgctxt "status interact error"
+msgid "Error: %{error}"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
+#, elixir-autogen, elixir-format
+msgctxt "status interact error message - status not found"
+msgid "Could not find status"
+msgstr ""
+
+#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
+#, elixir-autogen, elixir-format
+msgctxt "status interact error message - unknown error"
+msgid "Something went wrong."
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "status interact header"
+msgid "Interacting with %{nickname}'s %{status_link}"
+msgstr ""
+
+#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
+#, elixir-autogen, elixir-format
+msgctxt "status interact header - status link text"
+msgid "status"
+msgstr ""
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 0defdc74e..342ef9944 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -1,133 +1,171 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTML.Scrubber.Default do
@doc "The default HTML scrubbing policy: no "
require FastSanitize.Sanitizer.Meta
alias FastSanitize.Sanitizer.Meta
# credo:disable-for-previous-line
# No idea how to fix this one…
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.strip_comments()
Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_this_attribute_values(:a, "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url",
"mention hashtag"
])
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer",
"ugc"
])
Meta.allow_tag_with_these_attributes(:a, ["name", "title", "lang"])
Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"])
Meta.allow_tag_with_these_attributes(:acronym, ["title", "lang"])
# sort(1)-ed list
Meta.allow_tag_with_these_attributes(:bdi, [])
Meta.allow_tag_with_these_attributes(:bdo, ["dir"])
Meta.allow_tag_with_these_attributes(:big, ["lang"])
Meta.allow_tag_with_these_attributes(:b, ["lang"])
Meta.allow_tag_with_these_attributes(:blockquote, ["lang"])
Meta.allow_tag_with_these_attributes(:br, ["lang"])
Meta.allow_tag_with_these_attributes(:cite, ["lang"])
Meta.allow_tag_with_these_attributes(:code, ["lang"])
Meta.allow_tag_with_these_attributes(:del, ["lang"])
Meta.allow_tag_with_these_attributes(:dfn, ["lang"])
Meta.allow_tag_with_these_attributes(:em, ["lang"])
Meta.allow_tag_with_these_attributes(:hr, ["lang"])
Meta.allow_tag_with_these_attributes(:i, ["lang"])
Meta.allow_tag_with_these_attributes(:ins, ["lang"])
Meta.allow_tag_with_these_attributes(:kbd, ["lang"])
Meta.allow_tag_with_these_attributes(:li, ["lang"])
Meta.allow_tag_with_these_attributes(:ol, ["lang"])
Meta.allow_tag_with_these_attributes(:p, ["lang"])
Meta.allow_tag_with_these_attributes(:pre, ["lang"])
Meta.allow_tag_with_these_attributes(:q, ["lang"])
Meta.allow_tag_with_these_attributes(:rb, ["lang"])
Meta.allow_tag_with_these_attributes(:rp, ["lang"])
Meta.allow_tag_with_these_attributes(:rtc, ["lang"])
Meta.allow_tag_with_these_attributes(:rt, ["lang"])
Meta.allow_tag_with_these_attributes(:ruby, ["lang"])
Meta.allow_tag_with_these_attributes(:samp, ["lang"])
Meta.allow_tag_with_these_attributes(:s, ["lang"])
Meta.allow_tag_with_these_attributes(:small, ["lang"])
Meta.allow_tag_with_these_attributes(:strong, ["lang"])
Meta.allow_tag_with_these_attributes(:sub, ["lang"])
Meta.allow_tag_with_these_attributes(:sup, ["lang"])
Meta.allow_tag_with_these_attributes(:tt, ["lang"])
Meta.allow_tag_with_these_attributes(:u, ["lang"])
Meta.allow_tag_with_these_attributes(:ul, ["lang"])
Meta.allow_tag_with_these_attributes(:var, ["lang"])
Meta.allow_tag_with_these_attributes(:wbr, ["lang"])
Meta.allow_tag_with_this_attribute_values(:span, "class", [
"h-card",
"recipients-inline",
"quote-inline",
"invisible",
- "ellipsis"
+ "ellipsis",
+ "mfm-center",
+ "mfm-flip",
+ "mfm-font",
+ "mfm-blur",
+ "mfm-rotate",
+ "mfm-x2",
+ "mfm-x3",
+ "mfm-x4",
+ "mfm-position",
+ "mfm-scale",
+ "mfm-fg",
+ "mfm-bg",
+ "mfm-jelly",
+ "mfm-twitch",
+ "mfm-shake",
+ "mfm-spin",
+ "mfm-jump",
+ "mfm-bounce",
+ "mfm-rainbow",
+ "mfm-tada",
+ "mfm-sparkle"
])
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
- Meta.allow_tag_with_these_attributes(:span, ["lang"])
+ Meta.allow_tag_with_these_attributes(:span, [
+ "lang",
+ "data-mfm-h",
+ "data-mfm-v",
+ "data-mfm-x",
+ "data-mfm-y",
+ "data-mfm-alternate",
+ "data-mfm-speed",
+ "data-mfm-deg",
+ "data-mfm-left",
+ "data-mfm-serif",
+ "data-mfm-monospace",
+ "data-mfm-cursive",
+ "data-mfm-fantasy",
+ "data-mfm-emoji",
+ "data-mfm-math",
+ "data-mfm-color"
+ ])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
@allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
if @allow_inline_images do
Meta.allow_tag_with_this_attribute_values(:img, "class", ["emoji"])
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes(:img, [
"width",
"height",
"title",
"alt",
"lang"
])
end
if Pleroma.Config.get([:markup, :allow_tables]) do
Meta.allow_tag_with_these_attributes(:table, ["lang"])
Meta.allow_tag_with_these_attributes(:tbody, ["lang"])
Meta.allow_tag_with_these_attributes(:td, ["lang"])
Meta.allow_tag_with_these_attributes(:th, ["lang"])
Meta.allow_tag_with_these_attributes(:thead, ["lang"])
Meta.allow_tag_with_these_attributes(:tr, ["lang"])
end
if Pleroma.Config.get([:markup, :allow_headings]) do
Meta.allow_tag_with_these_attributes(:h1, ["lang"])
Meta.allow_tag_with_these_attributes(:h2, ["lang"])
Meta.allow_tag_with_these_attributes(:h3, ["lang"])
Meta.allow_tag_with_these_attributes(:h4, ["lang"])
Meta.allow_tag_with_these_attributes(:h5, ["lang"])
end
if Pleroma.Config.get([:markup, :allow_fonts]) do
Meta.allow_tag_with_these_attributes(:font, ["face", "lang"])
end
Meta.strip_everything_not_covered()
end
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
index ef1adc235..f672d8c13 100644
--- a/test/mix/tasks/pleroma/config_test.exs
+++ b/test/mix/tasks/pleroma/config_test.exs
@@ -1,367 +1,373 @@
# 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.ConfigTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Mix.Tasks.Pleroma.Config, as: MixTask
alias Pleroma.ConfigDB
alias Pleroma.Repo
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
Application.delete_env(:pleroma, :first_setting)
Application.delete_env(:pleroma, :second_setting)
end)
:ok
end
defp config_records do
ConfigDB
|> Repo.all()
|> Enum.sort()
end
defp insert_config_record(group, key, value) do
insert(:config,
group: group,
key: key,
value: value
)
end
test "error if file with custom settings doesn't exist" do
MixTask.migrate_to_db("config/non_existent_config_file.exs")
msg =
"To migrate settings, you must define custom settings in config/non_existent_config_file.exs."
assert_receive {:mix_shell, :info, [^msg]}, 15
end
describe "migrate_to_db/1" do
setup do
clear_config(:configurable_from_database, true)
end
test "config migration refused when deprecated settings are found" do
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
assert config_records() == []
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
assert_received {:mix_shell, :error, [message]}
assert message =~
"Migration is not allowed until all deprecation warnings have been resolved."
end
test "filtered settings are migrated to db" do
assert config_records() == []
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"})
refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"})
assert config1.value == [key: "value", key2: [Repo]]
assert config2.value == [key: "value2", key2: ["Activity"]]
end
test "config table is truncated before migration" do
insert_config_record(:pleroma, :first_setting, key: "value", key2: ["Activity"])
assert length(config_records()) == 1
MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
assert config.value == [key: "value", key2: [Repo]]
end
end
describe "with deletion of temp file" do
setup do
clear_config(:configurable_from_database, true)
temp_file = "config/temp.exported_from_db.secret.exs"
on_exit(fn ->
:ok = File.rm(temp_file)
end)
{:ok, temp_file: temp_file}
end
test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"])
insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo])
MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
assert config_records() == []
file = File.read!(temp_file)
assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second,"
end
test "load a settings with large values and pass to file", %{temp_file: temp_file} do
insert(:config,
key: :instance,
value: [
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
allow_relay: true,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
- allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+ allowed_post_formats: [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode",
+ "text/x.misskeymarkdown"
+ ],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: true,
multi_factor_authentication: [
totp: [
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
]
)
MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
assert config_records() == []
assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file)
assert file =~ "import Config\n"
assert file =~ "A Pleroma instance, an alternative fediverse server"
end
end
describe "migrate_from_db/1" do
setup do: clear_config(:configurable_from_database, true)
setup do
insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"])
insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo])
path = "test/instance_static"
file_path = Path.join(path, "temp.exported_from_db.secret.exs")
on_exit(fn -> File.rm!(file_path) end)
[file_path: file_path]
end
test "with path parameter", %{file_path: file_path} do
MixTask.run(["migrate_from_db", "--env", "temp", "--path", Path.dirname(file_path)])
file = File.read!(file_path)
assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second,"
end
test "release", %{file_path: file_path} do
clear_config(:release, true)
clear_config(:config_path, file_path)
MixTask.run(["migrate_from_db", "--env", "temp"])
file = File.read!(file_path)
assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second,"
end
end
describe "operations on database config" do
setup do: clear_config(:configurable_from_database, true)
test "dumping a specific group" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
"BOsPL-_KjNnjj_RMvLeR3dTOrcndi4TbMR0cu56gLGfGaT5m1gXxSfRHOcC4Dd78ycQL1gdhtx13qgKHmTM5xAI",
private_key: "Ism6FNdS31nLCA94EfVbJbDdJXCxAZ8cZiB1JQPN_t4"
)
MixTask.run(["dump", "pleroma"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
refute_receive {
:mix_shell,
:info,
[
"config :web_push_encryption, :vapid_details, [subject: \"mailto:administrator@example.com\", public_key: \"BOsPL-_KjNnjj_RMvLeR3dTOrcndi4TbMR0cu56gLGfGaT5m1gXxSfRHOcC4Dd78ycQL1gdhtx13qgKHmTM5xAI\", private_key: \"Ism6FNdS31nLCA94EfVbJbDdJXCxAZ8cZiB1JQPN_t4\"]\r\n\r\n"
]
}
# Ensure operations work when using atom syntax
MixTask.run(["dump", ":pleroma"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
end
test "dumping a specific key in a group" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
MixTask.run(["dump", "pleroma", "Pleroma.Captcha"])
refute_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
assert_receive {:mix_shell, :info,
["config :pleroma, Pleroma.Captcha, [enabled: false]\r\n\r\n"]}
end
test "dumps all configuration successfully" do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
MixTask.run(["dump"])
assert_receive {:mix_shell, :info,
["config :pleroma, :instance, [name: \"Pleroma Test\"]\r\n\r\n"]}
assert_receive {:mix_shell, :info,
["config :pleroma, Pleroma.Captcha, [enabled: false]\r\n\r\n"]}
end
end
describe "when configdb disabled" do
test "refuses to dump" do
clear_config(:configurable_from_database, false)
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
MixTask.run(["dump"])
msg =
"ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
assert_receive {:mix_shell, :error, [^msg]}
end
end
describe "destructive operations" do
setup do: clear_config(:configurable_from_database, true)
setup do
insert_config_record(:pleroma, :instance, name: "Pleroma Test")
insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
insert_config_record(:pleroma2, :key2, z: 1)
assert length(config_records()) == 3
:ok
end
test "deletes group of settings" do
MixTask.run(["delete", "--force", "pleroma"])
assert [%ConfigDB{group: :pleroma2, key: :key2}] = config_records()
end
test "deletes specified key" do
MixTask.run(["delete", "--force", "pleroma", "Pleroma.Captcha"])
assert [
%ConfigDB{group: :pleroma, key: :instance},
%ConfigDB{group: :pleroma2, key: :key2}
] = config_records()
end
test "resets entire config" do
MixTask.run(["reset", "--force"])
assert config_records() == []
end
test "filters non-whitelisted settings" do
clear_config(:database_config_whitelist, [
{:pleroma},
{:web_push_encryption, :vapid_details}
])
insert_config_record(:web_push_encryption, :non_whitelisted_key, a: 1)
insert_config_record(:web_push_encryption, :vapid_details, b: 1)
MixTask.run(["filter_whitelisted", "--force"])
assert [
%ConfigDB{group: :pleroma, key: :instance},
%ConfigDB{group: :pleroma, key: Pleroma.Captcha},
%ConfigDB{group: :web_push_encryption, key: :vapid_details}
] = config_records()
end
test "filter_whitelisted doesn't crash when whitelist is unset" do
clear_config(:database_config_whitelist, nil)
existing = config_records()
MixTask.run(["filter_whitelisted", "--force"])
assert config_records() == existing
end
test "filter_whitelisted doesn't crash when whitelist is disabled" do
clear_config(:database_config_whitelist, false)
existing = config_records()
MixTask.run(["filter_whitelisted", "--force"])
assert config_records() == existing
end
end
end
diff --git a/test/pleroma/dependency_version_test.exs b/test/pleroma/dependency_version_test.exs
new file mode 100644
index 000000000..0c9c1e939
--- /dev/null
+++ b/test/pleroma/dependency_version_test.exs
@@ -0,0 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.DependencyVersionTest do
+ use ExUnit.Case, async: true
+
+ test "uses majic 1.2" do
+ majic_version =
+ :majic
+ |> Application.spec(:vsn)
+ |> to_string()
+
+ assert Version.match?(majic_version, "~> 1.2")
+ end
+end
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 47f6f5f76..078bb643c 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -1,496 +1,516 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.MastodonWebsocketTest do
# Needs a streamer, needs to stay synchronous
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
alias Pleroma.Integration.WebsocketClient
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth
@moduletag needs_streamer: true, capture_log: true
@path Pleroma.Web.Endpoint.url()
|> URI.parse()
|> Map.put(:scheme, "ws")
|> Map.put(:path, "/api/v1/streaming")
|> URI.to_string()
def start_socket(qs \\ nil, headers \\ []) do
path =
case qs do
nil -> @path
qs -> @path <> qs
end
WebsocketClient.start_link(self(), path, headers)
end
defp decode_json(json) do
with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
{:ok, payload} <- Jason.decode(payload_text) do
{:ok, %{"event" => event, "payload" => payload}}
end
end
# Turns atom keys to strings
defp atom_key_to_string(json) do
json
|> Jason.encode!()
|> Jason.decode!()
end
test "refuses invalid requests" do
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk")
Process.sleep(30)
end)
end
test "requires authentication and a valid token for protected streams" do
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 401}} =
start_socket("?stream=user&access_token=aaaaaaaaaaaa")
assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user")
Process.sleep(30)
end)
end
test "allows unified stream" do
assert {:ok, _} = start_socket()
end
test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local")
assert {:ok, _} = start_socket("?stream=public:remote&instance=lain.com")
assert {:ok, _} = start_socket("?stream=hashtag&tag=lain")
end
test "receives well formatted events" do
user = insert(:user)
{:ok, _} = start_socket("?stream=public")
{:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
assert_receive {:text, raw_json}, 1_000
assert {:ok, json} = Jason.decode(raw_json)
assert "update" == json["event"]
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> atom_key_to_string()
assert json == view_json
end
describe "subscribing via WebSocket" do
test "can subscribe" do
user = insert(:user)
{:ok, pid} = start_socket()
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
{:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
assert_receive {:text, raw_json}, 1_000
assert {:ok, json} = Jason.decode(raw_json)
assert "update" == json["event"]
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> Jason.encode!()
|> Jason.decode!()
assert json == view_json
end
test "can subscribe to multiple streams" do
user = insert(:user)
{:ok, pid} = start_socket()
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(
pid,
%{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
{:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"})
assert_receive {:text, raw_json}, 1_000
assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json)
assert_receive {:text, raw_json}, 1_000
assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json)
streams = [stream1, stream2]
assert ["hashtag", "mew"] in streams
assert ["public"] in streams
end
test "won't double subscribe" do
user = insert(:user)
{:ok, pid} = start_socket()
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "ignored"}
}} = decode_json(raw_json)
{:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
assert_receive {:text, _}, 1_000
refute_receive {:text, _}, 1_000
end
test "rejects invalid streams" do
{:ok, pid} = start_socket()
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "nonsense"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "error", "error" => "bad_topic"}
}} = decode_json(raw_json)
end
test "can unsubscribe" do
user = insert(:user)
{:ok, pid} = start_socket()
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(pid, %{type: "unsubscribe", stream: "public"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "unsubscribe", "result" => "success"}
}} = decode_json(raw_json)
{:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"})
refute_receive {:text, _}, 1_000
end
end
describe "with a valid user token" do
setup do
{:ok, app} =
Pleroma.Repo.insert(
OAuth.App.register_changeset(%OAuth.App{}, %{
client_name: "client",
scopes: ["read"],
redirect_uris: "url"
})
)
user = insert(:user)
{:ok, auth} = OAuth.Authorization.create_authorization(app, user)
{:ok, token} = OAuth.Token.exchange_token(app, auth)
%{app: app, user: user, token: token}
end
test "accepts valid tokens", state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}")
end
test "accepts the 'user' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user")
Process.sleep(30)
end)
end
test "accepts the 'user:notification' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 401}} =
start_socket("?stream=user:notification")
Process.sleep(30)
end)
end
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 401}} =
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
Process.sleep(30)
end)
end
+ test "prefers sec-websocket-protocol token over query access_token", %{
+ token: token,
+ user: user
+ } do
+ assert {:ok, state} =
+ Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{
+ params: %{"stream" => "user", "access_token" => "invalid"},
+ connect_info: %{
+ sec_websocket_headers: [
+ {"sec-websocket-version", "13"},
+ {"sec-websocket-protocol", token.token}
+ ]
+ }
+ })
+
+ assert state.user.id == user.id
+ assert state.oauth_token.id == token.id
+ assert state.topics != []
+ end
+
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()
WebsocketClient.send_text(
pid,
%{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!())
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
end
test "rejects invalid token on client-sent event" do
assert {:ok, pid} = start_socket()
WebsocketClient.send_text(
pid,
%{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{
"type" => "pleroma:authenticate",
"result" => "error",
"error" => "unauthorized"
}
}} = decode_json(raw_json)
end
test "rejects new authenticate request if already logged-in", %{token: token} do
assert {:ok, pid} = start_socket()
WebsocketClient.send_text(
pid,
%{type: "pleroma:authenticate", token: token.token} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "pleroma:authenticate", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(
pid,
%{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{
"type" => "pleroma:authenticate",
"result" => "error",
"error" => "already_authenticated"
}
}} = decode_json(raw_json)
end
test "accepts the 'list' stream", %{token: token, user: user} do
posting_user = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "test"}, user)
Pleroma.List.follow(list, posting_user)
assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
assert {:ok, pid} = start_socket("?access_token=#{token.token}")
WebsocketClient.send_text(
pid,
%{type: "subscribe", stream: "list", list: list.id} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "success"}
}} = decode_json(raw_json)
WebsocketClient.send_text(
pid,
%{type: "subscribe", stream: "list", list: to_string(list.id)} |> Jason.encode!()
)
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "pleroma:respond",
"payload" => %{"type" => "subscribe", "result" => "ignored"}
}} = decode_json(raw_json)
end
test "disconnect when token is revoked", %{app: app, user: user, token: token} do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
{:ok, auth} = OAuth.Authorization.create_authorization(app, user)
{:ok, token2} = OAuth.Token.exchange_token(app, auth)
assert {:ok, _} = start_socket("?stream=user&access_token=#{token2.token}")
OAuth.Token.Strategy.Revoke.revoke(token)
assert_receive {:close, _}
assert_receive {:close, _}
refute_receive {:close, _}
end
test "receives private statuses", %{user: reading_user, token: token} do
user = insert(:user)
CommonAPI.follow(user, reading_user)
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
{:ok, activity} =
CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
assert_receive {:text, raw_json}, 1_000
assert {:ok, json} = Jason.decode(raw_json)
assert "update" == json["event"]
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
activity: activity,
for: reading_user
)
|> Jason.encode!()
|> Jason.decode!()
assert json == view_json
end
test "receives edits", %{user: reading_user, token: token} do
user = insert(:user)
CommonAPI.follow(user, reading_user)
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
{:ok, activity} =
CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"})
assert_receive {:text, _raw_json}, 1_000
{:ok, _} = CommonAPI.update(activity, user, %{status: "mew mew", visibility: "private"})
assert_receive {:text, raw_json}, 1_000
activity = Pleroma.Activity.normalize(activity)
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
activity: activity,
for: reading_user
)
|> Jason.encode!()
|> Jason.decode!()
assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
end
test "receives notifications", %{user: reading_user, token: token} do
user = insert(:user)
CommonAPI.follow(user, reading_user)
{:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
{:ok, %Pleroma.Activity{id: activity_id} = _activity} =
CommonAPI.post(user, %{
status: "nice echo chamber @#{reading_user.nickname}",
visibility: "private"
})
assert_receive {:text, raw_json}, 1_000
assert {:ok,
%{
"event" => "notification",
"payload" => %{
"status" => %{
"id" => ^activity_id
}
}
}} = decode_json(raw_json)
end
end
end
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
index 572d7acc3..0c7c4c840 100644
--- a/test/pleroma/signature_test.exs
+++ b/test/pleroma/signature_test.exs
@@ -1,144 +1,157 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SignatureTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Signature
+ alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
@private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----"
@public_key "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n"
@rsa_public_key {
:RSAPublicKey,
24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857,
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
65_537
}
defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\""
defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do
test "it returns key" do
expected_result = {:ok, @rsa_public_key}
user = insert(:user, public_key: @public_key)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
test "it returns error when not found user" do
assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == {:error, :error}
end
test "it returns error if public key is nil" do
user = insert(:user, public_key: nil)
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
end
end
describe "refetch_public_key/1" do
test "it returns key" do
ap_id = "https://mastodon.social/users/lambadalambda"
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
test "it returns error when not found user" do
assert capture_log(fn ->
{:error, _} = Signature.refetch_public_key(make_fake_conn("https://test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
describe "get_actor_id/1" do
test "it returns actor id" do
ap_id = "https://mastodon.social/users/lambadalambda"
assert Signature.get_actor_id(make_fake_conn(ap_id)) == {:ok, ap_id}
end
end
describe "sign/2" do
test "it returns signature headers" do
user =
insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda",
keys: @private_key
})
assert Signature.sign(
user,
%{
host: "test.test",
"content-length": 100
}
) ==
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
end
test "it returns error" do
user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
assert Signature.sign(
user,
%{host: "test.test", "content-length": 100}
) == {:error, []}
end
end
+ describe "validate_signature/1" do
+ test "treats HTTP signature errors as failed validation" do
+ conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
+
+ Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
+ {:error, :request_target_header}
+ end)
+
+ assert Signature.validate_signature(conn) == false
+ end
+ end
+
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
{:ok, "https://example.com/users/1234"}
end
test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it deduces the actor id for gotoSocial" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") ==
{:ok, "https://example.com/users/1234"}
end
test "it calls webfinger for 'acct:' accounts" do
with_mock(Pleroma.Web.WebFinger,
finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
) do
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
{:ok, "https://gensokyo.2hu/users/raymoo"}
end
end
end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end
diff --git a/test/pleroma/user/search_test.exs b/test/pleroma/user/search_test.exs
new file mode 100644
index 000000000..c1aca90bc
--- /dev/null
+++ b/test/pleroma/user/search_test.exs
@@ -0,0 +1,195 @@
+# Pleroma: A lightweight social networking server
+# Copyright © Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.SearchTest do
+ use Pleroma.DataCase, async: false
+
+ import Pleroma.Factory
+
+ alias Pleroma.Instances
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ describe "search/2 mention suggestions" do
+ test "prioritizes followed/follower users before others" do
+ user = insert(:user)
+
+ related =
+ insert(:user,
+ local: false,
+ nickname: "hj@real.example",
+ ap_id: "https://real.example/users/hj",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
+
+ {:ok, _related, _user} = User.follow(related, user)
+
+ results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [related.id, other.id]
+ end
+
+ test "orders followed/follower users by most recent activity" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "ali@remote.example",
+ ap_id: "https://remote.example/users/ali",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "alia@remote.example",
+ ap_id: "https://remote.example/users/alia",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("ali", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "groups followed/follower users first and sorts them by recency" do
+ user = insert(:user)
+
+ following_newest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontesta@related.example",
+ ap_id: "https://related.example/users/mentiontesta",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ follower_middle =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestb@related.example",
+ ap_id: "https://related.example/users/mentiontestb",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ mutual_oldest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestc@related.example",
+ ap_id: "https://related.example/users/mentiontestc",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ unrelated_newer =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestd@unrelated.example",
+ ap_id: "https://unrelated.example/users/mentiontestd",
+ last_status_at: ~N[2020-01-04 00:00:00]
+ )
+
+ {:ok, _user, _following_newest} = User.follow(user, following_newest)
+ {:ok, _follower_middle, _user} = User.follow(follower_middle, user)
+
+ {:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
+ {:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
+
+ results = User.search("mentiontest", for_user: user)
+
+ assert Enum.map(results, & &1.id) ==
+ [following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
+ end
+
+ test "uses last_active_at when last_status_at is missing" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "activefallbacka@remote.example",
+ ap_id: "https://remote.example/users/activefallbacka",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "activefallbackb@remote.example",
+ ap_id: "https://remote.example/users/activefallbackb",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("activefallback", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "does not return deactivated users even if related" do
+ user = insert(:user)
+
+ active =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtesta@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtesta",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ deactivated =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtestb@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtestb",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ {:ok, _user, _active} = User.follow(user, active)
+ {:ok, _user, _deactivated} = User.follow(user, deactivated)
+ Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
+
+ results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [active.id]
+ end
+
+ test "does not return users from unreachable instances" do
+ user = insert(:user)
+
+ {:ok, _instance} = Instances.set_unreachable("dead.example")
+
+ dead =
+ insert(:user,
+ local: false,
+ nickname: "ali@dead.example",
+ ap_id: "https://dead.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ alive =
+ insert(:user,
+ local: false,
+ nickname: "ali@alive.example",
+ ap_id: "https://alive.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _alive} = User.follow(user, alive)
+ {:ok, _user, _dead} = User.follow(user, dead)
+
+ results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [alive.id]
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index 3988c3912..68e39b3a0 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -1,2919 +1,2963 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Workers.ReceiverWorker
alias Pleroma.Workers.SignatureRetryWorker
import Pleroma.Factory
require Pleroma.Constants
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :federating], true)
defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do
assign_valid_signature_for_actor(conn, actor_id)
end
defp assign_valid_signature_for_actor(conn, actor) do
actor_id = Utils.get_ap_id(actor)
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"")
end
defp expect_signature_retry_from(%User{} = signer) do
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
Tesla.Mock.mock(fn
%{url: url} when url == signer.ap_id ->
%Tesla.Env{
status: 200,
body: Jason.encode!(signer_json),
headers: HttpRequestMock.activitypub_object_headers()
}
env ->
apply(HttpRequestMock, :request, [env])
end)
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
end
describe "/relay" do
setup do: clear_config([:instance, :allow_relay])
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
clear_config([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
end
describe "/internal/fetch" do
test "it returns the internal fetch user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(200)
assert res["id"] =~ "/fetch"
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(404)
end
end
describe "/users/:nickname" do
test "it returns a json representation of the user with accept application/json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/activity+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/ld+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns 404 for remote users", %{
conn: conn
} do
user = insert(:user, local: false, nickname: "remoteuser@example.com")
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}.json")
assert json_response(conn, 404)
end
test "it returns error when user is not found", %{conn: conn} do
response =
conn
|> put_req_header("accept", "application/json")
|> get("/users/jimm")
|> json_response(404)
assert response == "Not found"
end
end
describe "mastodon compatibility routes" do
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
{:ok, object} =
%{
"type" => "Note",
"content" => "hey",
"id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
"actor" => Endpoint.url() <> "/users/raymoo",
"to" => [Pleroma.Constants.as_public()]
}
|> Object.create()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/raymoo/statuses/999999999")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
end
test "it returns a json representation of the activity with accept application/json", %{
conn: conn
} do
{:ok, object} =
%{
"type" => "Note",
"content" => "hey",
"id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
"actor" => Endpoint.url() <> "/users/raymoo",
"to" => [Pleroma.Constants.as_public()]
}
|> Object.create()
{:ok, activity, _} =
%{
"id" => object.data["id"] <> "/activity",
"type" => "Create",
"object" => object.data["id"],
"actor" => object.data["actor"],
"to" => object.data["to"]
}
|> ActivityPub.persist(local: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/raymoo/statuses/999999999/activity")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
end
end
describe "/objects/:uuid" do
test "it doesn't return a local-only object", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "returns local-only objects when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
end
test "does not return local-only objects for remote users", %{conn: conn} do
user = insert(:user)
reader = insert(:user, local: false)
{:ok, post} =
CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, reader)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
json_response(response, 404)
end
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/activity+json",
%{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/ld+json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "does not cache authenticated response", %{conn: conn} do
user = insert(:user)
reader = insert(:user)
{:ok, post} =
CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, reader)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
json_response(response, 200)
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(404)
end
test "it returns 404 for non-public messages", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it returns 404 for tombstone objects", %{conn: conn} do
tombstone = insert(:tombstone)
uuid = String.split(tombstone.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after object deletion", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Object.delete(note)
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/objects/:uuid/replies" do
test "it renders the top-level collection", %{
conn: conn
} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, _} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies")
assert match?(
%{
"id" => _,
"type" => "OrderedCollection",
"totalItems" => 1,
"first" => %{
"id" => _,
"type" => "OrderedCollectionPage",
"orderedItems" => [_]
}
},
json_response(conn, 200)
)
end
test "first page id includes `?page=true`", %{conn: conn} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, _} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies")
%{"id" => collection_id, "first" => %{"id" => page_id, "partOf" => part_of}} =
json_response(conn, 200)
assert part_of == collection_id
assert String.contains?(page_id, "page=true")
end
test "unknown query params do not crash the endpoint", %{conn: conn} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, _} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies?unknown_param=1")
assert %{"type" => "OrderedCollection"} = json_response(conn, 200)
end
test "it renders a collection page", %{
conn: conn
} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, r1} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
{:ok, r2} =
CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: note.id})
{:ok, _} =
CommonAPI.post(user, %{status: "reply3", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies?page=true&min_id=#{r1.object.id}&limit=1")
expected_uris = [r2.object.data["id"]]
assert match?(
%{
"id" => _,
"type" => "OrderedCollectionPage",
"prev" => _,
"next" => _,
"orderedItems" => ^expected_uris
},
json_response(conn, 200)
)
end
end
describe "/activities/:uuid" do
test "it does not include a top-level replies collection on activities", %{conn: conn} do
clear_config([:activitypub, :note_replies_output_limit], 1)
activity = insert(:note_activity)
activity = Activity.get_by_id_with_object(activity.id)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
res = json_response(conn, 200)
refute Map.has_key?(res, "replies")
assert get_in(res, ["object", "replies", "id"]) == activity.object.data["id"] <> "/replies"
end
test "it doesn't return a local-only activity", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/activities/#{uuid}")
assert json_response(conn, 404)
end
test "returns local-only activities when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
end
test "it returns a json representation of the activity", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
end
test "it returns 404 for non-public activities", %{conn: conn} do
activity = insert(:direct_note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 404)
end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note_activity)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it caches a response", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after activity deletion", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Activity.delete_all_by_object_ap_id(activity.object.data["id"])
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db",
%{conn: conn} do
user =
insert(:user,
ap_id: "https://mastodon.example.org/users/raymoo",
local: false,
last_refreshed_at: nil
)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> put_in(["object", "attributedTo"], user.ap_id)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "does not create a forged post after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/inbox-forged-note"
data = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-create",
"context" => "https://two.com/contexts/inbox-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/inbox-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "does not create a forged like after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
data = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
test "does not delete an object after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
object_id = note.data["id"]
data = %{
"type" => "Delete",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-delete",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => object_id
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id)
end
test "does not create a forged post signed by a different actor", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/inbox-signed-forged-note"
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-signed-forged-create",
"context" => "https://two.com/contexts/inbox-signed-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/inbox-signed-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_retry_from(alice)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header(
"signature",
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
)
|> post("/inbox", data)
assert conn.assigns.valid_signature == false
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "does not create a forged like signed by a different actor", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-signed-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
expect_signature_retry_from(alice)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header(
"signature",
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
)
|> post("/inbox", data)
assert conn.assigns.valid_signature == false
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
+ test "does not process post with Host header not for us", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ object_id = "https://one.com/objects/inbox-forged-note"
+
+ data = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/inbox-forged-create",
+ "context" => "https://one.com/contexts/inbox-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/inbox-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ # Plug will complain when replacing raw host header with put_req_header.
+ # The Plug way is updating conn.host, but that isn't the raw header
+ # and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port.
+ conn =
+ conn
+ |> assign_valid_signature_for_actor(alice)
+ |> delete_req_header("host")
+ |> put_req_header("content-type", "application/activity+json")
+
+ conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]}
+ conn = post(conn, "/inbox", data)
+
+ assert "Host header does not match this instance" == conn.resp_body
+ assert 400 == conn.status
+ assert true == conn.halted
+
+ refute Activity.get_by_ap_id(data["id"])
+ refute Object.get_by_ap_id(object_id)
+ end
+
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()
assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
relay = refresh_record(relay)
accept =
File.read!("test/fixtures/relay/accept-follow.json")
|> String.replace("{{ap_id}}", relay.ap_id)
|> String.replace("{{activity_id}}", activity.data["id"])
assert "ok" ==
conn
|> assign_valid_signature_for_actor(followed_relay)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Pleroma.FollowingRelationship.following?(
relay,
followed_relay
)
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok = Mix.Tasks.Pleroma.Relay.run(["list"])
assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
end
test "without valid signature, " <>
"it accepts Create activities and requires enabled federation",
%{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
conn = put_req_header(conn, "content-type", "application/activity+json")
clear_config([:instance, :federating], false)
conn
|> post("/inbox", data)
|> json_response(403)
conn
|> post("/inbox", non_create_data)
|> json_response(403)
clear_config([:instance, :federating], true)
ret_conn = post(conn, "/inbox", data)
assert "ok" == json_response(ret_conn, 200)
conn
|> post("/inbox", non_create_data)
|> json_response(400)
end
# When activity is delivered to the inbox and we cannot immediately verify signature
# we capture all the params and process it later in the Oban job.
# Once we begin processing it through Oban we risk fetching the actor to validate the
# activity which just leads to inserting a new user to process a Delete not relevant to us.
test "Activities of certain types from an unknown actor are discarded", %{conn: conn} do
example_bad_types =
Pleroma.Constants.activity_types() --
Pleroma.Constants.allowed_activity_types_from_strangers()
Enum.each(example_bad_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody"
}
|> Jason.encode!()
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "Unknown activity types are discarded", %{conn: conn} do
unknown_types = ["Poke", "Read", "Dazzle"]
actor =
insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody")
Enum.each(unknown_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => actor.ap_id
}
|> Jason.encode!()
conn
|> assign_valid_signature_for_actor(actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
status =
File.read!("test/fixtures/statuses/note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)
object_url = "https://example.com/objects/#{object_id}"
user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
Tesla.Mock.mock(fn
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add",
"to" => [Pleroma.Constants.as_public()]
}
assert "ok" ==
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]
data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()]
}
assert "ok" ==
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end
test "mastodon pin/unpin", %{conn: conn} do
status_id = "105786274556060421"
status =
File.read!("test/fixtures/statuses/masto-note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{status_id}}", status_id)
status_url = "https://example.com/users/lain/statuses/#{status_id}"
user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
Tesla.Mock.mock(fn
%{
method: :get,
url: ^status_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add"
}
assert "ok" ==
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]
data = %{
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove"
}
assert "ok" ==
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end
end
describe "/users/:nickname/inbox" do
setup do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
[data: data]
end
test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("bcc", [user.ap_id])
|> Kernel.put_in(["object", "bcc"], [user.ap_id])
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", user.ap_id)
|> Map.put("cc", [])
|> Kernel.put_in(["object", "to"], user.ap_id)
|> Kernel.put_in(["object", "cc"], [])
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", [])
|> Map.put("cc", user.ap_id)
|> Kernel.put_in(["object", "to"], [])
|> Kernel.put_in(["object", "cc"], user.ap_id)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
%Activity{} = activity = Activity.get_by_ap_id(data["id"])
assert user.ap_id in activity.recipients
end
test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", [])
|> Map.put("cc", [])
|> Map.put("bcc", user.ap_id)
|> Kernel.put_in(["object", "to"], [])
|> Kernel.put_in(["object", "cc"], [])
|> Kernel.put_in(["object", "bcc"], user.ap_id)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts announces with to as string instead of array", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "hey"})
announcer = insert(:user, local: false)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => announcer.ap_id,
"id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
"object" => post.data["object"],
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => [user.ap_id],
"type" => "Announce"
}
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
%Activity{} = activity = Activity.get_by_ap_id(data["id"])
assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
end
test "it accepts messages from actors that are followed by the user", %{
conn: conn,
data: data
} do
recipient = insert(:user)
actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
{:ok, recipient, actor} = User.follow(recipient, actor)
object =
data["object"]
|> Map.put("attributedTo", actor.ap_id)
data =
data
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it rejects reads from other users", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, other_user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
to = [
recipient.ap_id,
recipient.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
]
cc = [recipient.follower_address, actor.follower_address]
data = %{
"@context" => ["https://www.w3.org/ns/activitystreams"],
"type" => "Create",
"id" => Utils.generate_activity_id(),
"to" => to,
"cc" => cc,
"actor" => actor.ap_id,
"object" => %{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => "It's a note",
"attributedTo" => actor.ap_id,
"id" => Utils.generate_object_id()
}
}
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert activity = Activity.get_by_ap_id(data["id"])
assert activity.id
assert actor.follower_address in activity.recipients
assert actor.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.recipients
refute recipient.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.data["to"]
end
test "it requires authentication", %{conn: conn} do
user = insert(:user)
conn = put_req_header(conn, "accept", "application/activity+json")
ret_conn = get(conn, "/users/#{user.nickname}/inbox")
assert json_response(ret_conn, 403)
ret_conn =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/inbox")
assert json_response(ret_conn, 200)
end
test "forwarded report", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
remote_domain = URI.parse(actor.ap_id).host
reported_user = insert(:user)
note = insert(:note_activity, user: reported_user)
data = %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://#{remote_domain}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
],
"actor" => actor.ap_id,
# CC and TO might either not exist at all, or be empty. We should be able to handle either.
# "cc" => [],
"content" => "test",
"context" => "context",
"id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
"nickname" => reported_user.nickname,
"object" => [
reported_user.ap_id,
%{
"actor" => %{
"actor_type" => "Person",
"approval_pending" => false,
"avatar" => "",
"confirmation_pending" => false,
"deactivated" => false,
"display_name" => "test user",
"id" => reported_user.id,
"local" => false,
"nickname" => reported_user.nickname,
"registration_reason" => nil,
"roles" => %{
"admin" => false,
"moderator" => false
},
"tags" => [],
"url" => reported_user.ap_id
},
"content" => "",
"id" => note.data["id"],
"published" => note.data["published"],
"type" => "Note"
}
],
"published" => note.data["published"],
"state" => "open",
"to" => [],
"type" => "Flag"
}
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
ObanHelpers.perform_all()
Swoosh.TestAssertions.assert_email_sent(
to: {admin.name, admin.email},
html_body: ~r/Reported Account:/i
)
end
test "forwarded report from mastodon", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
remote_domain = URI.parse(actor.ap_id).host
remote_actor = "https://#{remote_domain}/actor"
[reported_user, another] = insert_list(2, :user)
note = insert(:note_activity, user: reported_user)
Pleroma.Web.CommonAPI.favorite(note.id, another)
mock_json_body =
"test/fixtures/mastodon/application_actor.json"
|> File.read!()
|> String.replace("{{DOMAIN}}", remote_domain)
Tesla.Mock.mock(fn %{url: ^remote_actor} ->
%Tesla.Env{
status: 200,
body: mock_json_body,
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => remote_actor,
"content" => "test report",
"id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
"object" => [
reported_user.ap_id,
note.data["object"]
],
"type" => "Flag"
}
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
reported_user_ap_id = reported_user.ap_id
[^reported_user_ap_id, flag_data] = flag_activity.data["object"]
Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
ObanHelpers.perform_all()
Swoosh.TestAssertions.assert_email_sent(
to: {admin.name, admin.email},
html_body: ~r/#{note.data["object"]}/i
)
end
test "it accepts an incoming Block", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("type", "Block")
|> Map.put("to", [user.ap_id])
|> Map.put("cc", [])
|> Map.put("object", user.ap_id)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it returns an error when receiving an activity sent to a deactivated user", %{
conn: conn,
data: data
} do
user = insert(:user)
{:ok, _} = User.set_activation(user, false)
data =
data
|> Map.put("bcc", [user.ap_id])
|> Kernel.put_in(["object", "bcc"], [user.ap_id])
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "User deactivated" == json_response(conn, 404)
end
test "it returns an error when receiving an activity sent from a deactivated user", %{
conn: conn,
data: data
} do
sender = insert(:user)
user = insert(:user)
{:ok, _} = User.set_activation(sender, false)
data =
data
|> Map.put("bcc", [user.ap_id])
|> Map.put("actor", sender.ap_id)
|> Kernel.put_in(["object", "bcc"], [user.ap_id])
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "Sender deactivated" == json_response(conn, 404)
end
end
describe "GET /users/:nickname/outbox" do
setup do
Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
:ok
end
test "it paginates correctly", %{conn: conn} do
user = insert(:user)
conn = assign(conn, :user, user)
outbox_endpoint = user.ap_id <> "/outbox"
_posts =
for i <- 0..25 do
{:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
activity
end
result =
conn
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint <> "?page=true")
|> json_response(200)
result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
assert length(result["orderedItems"]) == 20
assert length(result_ids) == 20
assert result["next"]
assert String.starts_with?(result["next"], outbox_endpoint)
result_next =
conn
|> put_req_header("accept", "application/activity+json")
|> get(result["next"])
|> json_response(200)
result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
assert length(result_next["orderedItems"]) == 6
assert length(result_next_ids) == 6
refute Enum.find(result_next_ids, fn x -> x in result_ids end)
refute Enum.find(result_ids, fn x -> x in result_next_ids end)
assert String.starts_with?(result["id"], outbox_endpoint)
result_next_again =
conn
|> put_req_header("accept", "application/activity+json")
|> get(result_next["id"])
|> json_response(200)
assert result_next == result_next_again
end
test "it returns 200 even if there're no activities", %{conn: conn} do
user = insert(:user)
outbox_endpoint = user.ap_id <> "/outbox"
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint)
result = json_response(conn, 200)
assert outbox_endpoint == result["id"]
end
test "it returns a local note activity when authenticated as local user", %{conn: conn} do
user = insert(:user)
reader = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
ap_id = note_activity.data["id"]
resp =
conn
|> assign(:user, reader)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
|> json_response(200)
assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp
end
test "it does not return a local note activity when unauthenticated", %{conn: conn} do
user = insert(:user)
{:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
resp =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
|> json_response(200)
assert %{"orderedItems" => []} = resp
end
test "it does not return a local note activity when C2S API is disabled", %{conn: conn} do
clear_config([:activitypub, :client_api_enabled], false)
user = insert(:user)
reader = insert(:user)
{:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"})
resp =
conn
|> assign(:user, reader)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
|> json_response(200)
assert %{"orderedItems" => []} = resp
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it returns an announce activity in a collection", %{conn: conn} do
announce_activity = insert(:announce_activity)
user = User.get_cached_by_ap_id(announce_activity.data["actor"])
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"]
end
test "It returns poll Answers when authenticated", %{conn: conn} do
poller = insert(:user)
voter = insert(:user)
{:ok, activity} =
CommonAPI.post(poller, %{
status: "suya...",
poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
})
assert question = Object.normalize(activity, fetch: false)
{:ok, [activity], _object} = CommonAPI.vote(question, voter, [1])
assert outbox_get =
conn
|> assign(:user, voter)
|> put_req_header("accept", "application/activity+json")
|> get(voter.ap_id <> "/outbox?page=true")
|> json_response(200)
assert [answer_outbox] = outbox_get["orderedItems"]
assert answer_outbox["id"] == activity.data["id"]
end
test "it works with authorized fetch forced when authenticated" do
clear_config([:activitypub, :authorized_fetch_mode], true)
user = insert(:user)
outbox_endpoint = user.ap_id <> "/outbox"
conn =
build_conn()
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint)
assert json_response(conn, 200)
end
test "it fails with authorized fetch forced when unauthenticated", %{conn: conn} do
clear_config([:activitypub, :authorized_fetch_mode], true)
user = insert(:user)
outbox_endpoint = user.ap_id <> "/outbox"
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint)
assert response(conn, 401)
end
end
describe "POST /users/:nickname/outbox (C2S)" do
setup do: clear_config([:instance, :limit])
setup do
[
activity: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test",
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
]
end
test "it rejects posts from other users / unauthenticated users", %{
conn: conn,
activity: activity
} do
user = insert(:user)
other_user = insert(:user)
conn = put_req_header(conn, "content-type", "application/activity+json")
conn
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
conn
|> assign(:user, other_user)
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
end
test "it inserts an incoming create activity into the database", %{
conn: conn,
activity: activity
} do
user = insert(:user)
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(201)
assert Activity.get_by_ap_id(result["id"])
assert result["object"]
assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
assert object["content"] == activity["object"]["content"]
end
test "it inserts an incoming reply create activity into the database", %{conn: conn} do
user = insert(:user)
replying_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
data = %{
type: "Create",
object: %{
to: [Pleroma.Constants.as_public(), user.ap_id],
cc: [replying_user.follower_address],
inReplyTo: activity.object.data["id"],
content: "green tea",
type: "Note"
}
}
result =
conn
|> assign(:user, replying_user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{replying_user.nickname}/outbox", data)
|> json_response(201)
updated_object = Object.normalize(activity.object.data["id"], fetch: false)
assert Activity.get_by_ap_id(result["id"])
assert result["object"]
assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
assert object["content"] == data.object.content
assert Pleroma.Web.ActivityPub.Visibility.public?(object)
assert object["inReplyTo"] == activity.object.data["id"]
assert updated_object.data["repliesCount"] == 1
end
test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user)
activity =
activity
|> put_in(["object", "type"], "Benis")
_result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
end
test "it inserts an incoming sensitive activity into the database", %{
conn: conn,
activity: activity
} do
user = insert(:user)
conn = assign(conn, :user, user)
object = Map.put(activity["object"], "sensitive", true)
activity = Map.put(activity, "object", object)
response =
conn
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(201)
assert Activity.get_by_ap_id(response["id"])
assert response["object"]
assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
assert response_object["sensitive"] == true
assert response_object["content"] == activity["object"]["content"]
representation =
conn
|> put_req_header("accept", "application/activity+json")
|> get(response["id"])
|> json_response(200)
assert representation["object"]["sensitive"] == true
end
test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
user = insert(:user)
activity = Map.put(activity, "type", "BadType")
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
assert json_response(conn, 400)
end
test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
"type" => "Delete",
"object" => %{
"id" => note_object.data["id"]
}
}
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
|> json_response(201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["type"] == "Tombstone"
end
test "it rejects delete activity of object from other actor", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = insert(:user)
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects update activity of object from other actor", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = insert(:user)
data = %{
type: "Update",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
assert note_object == Object.normalize(note_activity, fetch: false)
end
test "it rejects Add to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Add",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects Remove to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Remove",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects updating Actor's profile", %{conn: conn} do
user = insert(:user, local: true)
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_new = Map.put(user_object, "name", "lain")
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
updated_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert updated_user_object == user_object
assert json_response(conn, 400)
end
# Actor publicKey tests are redundant with above test,
# left here for the case that Updating Actors is ever supported
test "it rejects updating Actor's publicKey", %{conn: conn} do
user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_public_key = Map.fetch!(user_object, "publicKey")
user_object_public_key = Map.put(user_object_public_key, "publicKeyPem", public_key)
user_object_new = Map.put(user_object, "publicKey", user_object_public_key)
refute user_object == user_object_new
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
new_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert user_object == new_user_object
assert json_response(conn, 400)
end
test "it rejects updating Actor's publicKey of another user", %{conn: conn} do
user = insert(:user)
target_user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
target_user_object_public_key = Map.fetch!(target_user_object, "publicKey")
target_user_object_public_key =
Map.put(target_user_object_public_key, "publicKeyPem", public_key)
target_user_object_new =
Map.put(target_user_object, "publicKey", target_user_object_public_key)
refute target_user_object == target_user_object_new
data = %{
type: "Update",
object: target_user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{target_user.nickname}/outbox", data)
new_target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
assert target_user_object == new_target_user_object
assert json_response(conn, 403)
end
test "it rejects creating Actors of type Application", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Application"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Person", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Person"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Service", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Service"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects like activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Like",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects announce activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Announce",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects emojireact activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "EmojiReact",
object: %{
id: post_object.data["id"]
},
content: "😀"
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Like",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["like_count"] == 1
end
test "it doesn't spreads faulty attributedTo or actor fields", %{
conn: conn,
activity: activity
} do
reimu = insert(:user, nickname: "reimu")
cirno = insert(:user, nickname: "cirno")
assert reimu.ap_id
assert cirno.ap_id
activity =
activity
|> put_in(["object", "actor"], reimu.ap_id)
|> put_in(["object", "attributedTo"], reimu.ap_id)
|> put_in(["actor"], reimu.ap_id)
|> put_in(["attributedTo"], reimu.ap_id)
_reimu_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reimu.nickname}/outbox", activity)
|> json_response(403)
cirno_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{cirno.nickname}/outbox", activity)
|> json_response(201)
assert cirno_outbox["attributedTo"] == nil
assert cirno_outbox["actor"] == cirno.ap_id
assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
assert cirno_object.data["actor"] == cirno.ap_id
assert cirno_object.data["attributedTo"] == cirno.ap_id
end
test "Character limitation", %{conn: conn, activity: activity} do
clear_config([:instance, :limit], 5)
user = insert(:user)
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
assert result == "Character limit (5 characters) exceeded, contains 11 characters"
end
end
describe "/relay/followers" do
test "it returns relay followers", %{conn: conn} do
relay_actor = Relay.get_actor()
user = insert(:user)
User.follow(user, relay_actor)
result =
conn
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get("/relay/followers")
|> json_response(404)
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get("/relay/following")
|> json_response(404)
end
end
describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> assign(:user, user_two)
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_followers: true)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
%{conn: conn} do
user = insert(:user)
other_user = insert(:user, hide_followers: true)
result =
conn
|> assign(:user, user)
|> get("/users/#{other_user.nickname}/followers?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
%{conn: conn} do
user = insert(:user, hide_followers: true)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
other_user = insert(:user)
User.follow(other_user, user)
end)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
end
end
describe "/users/:nickname/following" do
test "it returns the following in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert result["first"]["orderedItems"] == [user_two.ap_id]
end
test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_follows: true)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/following")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
%{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_follows: true)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/following?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
%{conn: conn} do
user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
user = User.get_cached_by_id(user.id)
other_user = insert(:user)
User.follow(user, other_user)
end)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
end
end
describe "delivery tracking" do
test "it tracks a signed object fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed activity fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
end
describe "Additional ActivityPub C2S endpoints" do
test "GET /api/ap/whoami", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/ap/whoami")
user = User.get_cached_by_id(user.id)
assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
conn
|> get("/api/ap/whoami")
|> json_response(403)
end
setup do: clear_config([:media_proxy])
setup do: clear_config([Pleroma.Upload])
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
@tag :skip_darwin
test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
object =
conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(:created)
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
assert is_binary(object_href)
assert object_mediatype == "image/jpeg"
assert String.ends_with?(object_href, ".jpg")
activity_request = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test, attachment",
"attachment" => [object],
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
activity_response =
conn
|> assign(:user, user)
|> post("/users/#{user.nickname}/outbox", activity_request)
|> json_response(:created)
assert activity_response["id"]
assert activity_response["object"]
assert activity_response["actor"] == user.ap_id
assert %Object{data: %{"attachment" => [attachment]}} =
Object.normalize(activity_response["object"], fetch: false)
assert attachment["type"] == "Document"
assert attachment["name"] == desc
assert [
%{
"href" => ^object_href,
"type" => "Link",
"mediaType" => ^object_mediatype
}
] = attachment["url"]
# Fails if unauthenticated
conn
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403)
end
test "they don't work when C2S API is disabled", %{conn: conn} do
clear_config([:activitypub, :client_api_enabled], false)
user = insert(:user)
assert conn
|> assign(:user, user)
|> get("/api/ap/whoami")
|> response(403)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> response(403)
end
end
test "pinned collection", %{conn: conn} do
clear_config([:instance, :max_pinned_statuses], 2)
user = insert(:user)
objects = insert_list(2, :note, user: user)
Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user ->
{:ok, updated} = User.add_pinned_object_id(user, object_id)
updated
end)
%{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} =
refresh_record(user)
%{"id" => ^featured_address, "orderedItems" => items, "totalItems" => 2} =
conn
|> get("/users/#{nickname}/collections/featured")
|> json_response(200)
object_ids = Enum.map(items, & &1["id"])
assert Enum.all?(pinned_objects, fn {obj_id, _} ->
obj_id in object_ids
end)
end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c32811c5b..bf9c70fb6 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -1,309 +1,474 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do
use Pleroma.DataCase, async: true
alias Pleroma.Language.LanguageDetectorMock
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.Utils
import Mox
import Pleroma.Factory
# Setup for all tests
setup do
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
# Stub the LanguageDetectorMock with default implementations
LanguageDetectorMock
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
|> stub(:detect, fn _text -> nil end)
:ok
end
describe "Notes" do
setup do
user = insert(:user)
note = %{
"id" => Utils.generate_activity_id(),
"type" => "Note",
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => [],
"content" => "Hellow this is content.",
"context" => "xxx",
"summary" => "a post"
}
%{user: user, note: note}
end
test "a basic note validates", %{note: note} do
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a note from factory validates" do
note = insert(:note)
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note.data)
end
end
describe "Note with history" do
setup do
user = insert(:user)
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
{:ok, %{"object" => external_rep}} =
Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
%{external_rep: external_rep}
end
test "edited note", %{external_rep: external_rep} do
assert %{"formerRepresentations" => %{"orderedItems" => [%{"tag" => [_]}]}} = external_rep
{:ok, validate_res, []} = ObjectValidator.validate(external_rep, [])
assert %{"formerRepresentations" => %{"orderedItems" => [%{"emoji" => %{"dinosaur" => _}}]}} =
validate_res
end
test "edited note, badly-formed formerRepresentations", %{external_rep: external_rep} do
external_rep = Map.put(external_rep, "formerRepresentations", %{})
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
test "edited note, badly-formed history item", %{external_rep: external_rep} do
history_item =
Enum.at(external_rep["formerRepresentations"]["orderedItems"], 0)
|> Map.put("type", "Foo")
external_rep =
put_in(
external_rep,
["formerRepresentations", "orderedItems"],
[history_item]
)
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
end
test "a Note from Roadhouse validates" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")
%{"object" => note} =
"test/fixtures/roadhouse-create-activity.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Note from Convergence AP Bridge validates" do
insert(:user, ap_id: "https://cc.mkdir.uk/ap/acct/hiira")
note =
"test/fixtures/ccworld-ap-bridge_note.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a note with an attachment should work", _ do
insert(:user, %{ap_id: "https://owncast.localhost.localdomain/federation/user/streamer"})
note =
"test/fixtures/owncast-note-with-attachment.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Note without replies/first/items validates" do
insert(:user, ap_id: "https://mastodon.social/users/emelie")
note =
"test/fixtures/tesla_mock/status.emelie.json"
|> File.read!()
|> Jason.decode!()
|> pop_in(["replies", "first", "items"])
|> elem(1)
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+ test "a Misskey MFM note is rendered from source content" do
+ user = insert(:user, ap_id: "https://misskey.example/users/alice")
+
+ note = %{
+ "id" => "https://misskey.example/notes/1",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => "$[spin.speed=1s mfm goes here] <script>alert('xss')</script>",
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert source["mediaType"] == "text/x.misskeymarkdown"
+ assert content =~ ~s(class="mfm-spin")
+ assert content =~ ~s(data-mfm-speed="1s")
+ assert content =~ "mfm goes here"
+ refute content =~ "original content"
+ refute content =~ "<script"
+ end
+
+ test "a Misskey MFM note resolves only cached AP mention tags" do
+ remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
+ local_user = insert(:user, nickname: "local_user")
+
+ note = %{
+ "id" => "https://misskey.example/notes/3",
+ "type" => "Note",
+ "actor" => remote_user.ap_id,
+ "attributedTo" => remote_user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "tag" => [
+ %{
+ "type" => "Mention",
+ "name" => "@local_user",
+ "href" => local_user.ap_id
+ },
+ %{
+ "type" => "Mention",
+ "name" => "@uncached",
+ "href" => "https://misskey.example/users/uncached"
+ }
+ ],
+ "source" => %{
+ "content" => "@local_user @uncached $[spin hello]",
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content =~ local_user.ap_id
+ assert content =~ "@uncached"
+ end
+
+ test "a Misskey MFM note drops oversized source content instead of parsing it" do
+ user = insert(:user, ap_id: "https://misskey.example/users/oversized")
+
+ note = %{
+ "id" => "https://misskey.example/notes/4",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "<span class=\"mfm-spin\">safe fallback</span>",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => String.duplicate("x", 5_001),
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content == "<span class=\"mfm-spin\">safe fallback</span>"
+ refute Map.has_key?(source, "content")
+ end
+
+ test "a note drops oversized non-MFM source content" do
+ user = insert(:user, ap_id: "https://example.com/users/source")
+
+ note = %{
+ "id" => "https://example.com/notes/1",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "regular content",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => String.duplicate("x", 5_001),
+ "mediaType" => "text/markdown"
+ }
+ }
+
+ %{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note)
+
+ assert source == %{"mediaType" => "text/markdown"}
+ end
+
+ test "a Misskey MFM note with legacy _misskey_content is rendered" do
+ user = insert(:user, ap_id: "https://misskey.example/users/legacy")
+
+ note = %{
+ "id" => "https://misskey.example/notes/5",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "_misskey_content" => "$[spin legacy]"
+ }
+
+ %{valid?: true, changes: %{content: content, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"}
+ assert content =~ ~s(class="mfm-spin")
+ assert content =~ "legacy"
+ end
+
+ test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do
+ user = insert(:user, ap_id: "https://misskey.example/users/bob")
+
+ note = %{
+ "id" => "https://misskey.example/notes/2",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" =>
+ "<span class=\"mfm-spin\">already rendered</span><script>alert('xss')</script>",
+ "htmlMfm" => true,
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => String.duplicate("x", 5_001),
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content == "<span class=\"mfm-spin\">already rendered</span>alert(&#39;xss&#39;)"
+ refute Map.has_key?(source, "content")
+ end
+
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")
%{"object" => note} =
"test/fixtures/mastodon-update-with-likes.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "Fedibird quote post" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
end
test "Fedibird quote post with quoteUri field" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729"
end
test "Misskey quote post" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
end
test "Parse tag as quote" do
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
insert(:user, ap_id: "https://server.example/users/1")
data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://server.example/objects/123"
assert Enum.at(cng.changes.tag, 0).changes == %{
type: "Link",
mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
href: "https://server.example/objects/123",
name: "RE: https://server.example/objects/123"
}
end
describe "Note language" do
test "it detects language from JSON-LD context" do
user = insert(:user)
note_activity = %{
"@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
assert meta[:object_data]["language"] == "pl"
end
test "it detects language from contentMap" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"contentMap" => %{
"de" => "Gott segne",
"pl" => "Szczęść Boże"
},
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.language == "pl"
end
test "it doesn't call LanguageDetector when language is specified" do
# Set up expectation that detect should not be called
LanguageDetectorMock
|> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end)
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "a post in English",
"contentMap" => %{
"en" => "a post in English"
},
"attributedTo" => user.ap_id
}
ArticleNotePageValidator.cast_and_apply(note)
end
test "it adds contentMap if language is specified" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "тест",
"language" => "uk",
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.contentMap == %{
"uk" => "тест"
}
end
end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index c1e01557d..bedb466a4 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -1,1119 +1,1156 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Mock
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do
test "it works for incoming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Jason.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Jason.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it accepts Flag activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
object = Object.normalize(activity, fetch: false)
note_obj = %{
"type" => "Note",
"id" => activity.object.data["id"],
"content" => "test post",
"published" => object.data["published"],
"actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true})
}
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => [user.ap_id, activity.data["id"]],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["object"] == [user.ap_id, note_obj]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
+ test "it rejects Flag activities when both reporter and reported account are remote" do
+ reporter = insert(:user, local: false, domain: "mastodon.cat")
+ reported = insert(:user, local: false, domain: "nicecrew.digital")
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => reporter.ap_id,
+ "content" => "blocked AND reported!!!",
+ "object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
+ "type" => "Flag"
+ }
+
+ assert {:reject, reason} = Transmogrifier.handle_incoming(message)
+ assert reason =~ "third-party report"
+ refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
+ end
+
+ test "it accepts Flag activities with just actor id as object" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "cc" => [user.ap_id],
+ "object" => user.ap_id,
+ "type" => "Flag",
+ "content" => "blocked AND reported!!!",
+ "actor" => other_user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert activity.data["content"] == "blocked AND reported!!!"
+ assert activity.data["actor"] == other_user.ap_id
+ assert activity.data["cc"] == [user.ap_id]
+ end
+
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Move",
"actor" => old_user.ap_id,
"object" => old_user.ap_id,
"target" => new_user.ap_id
}
assert :error = Transmogrifier.handle_incoming(message)
{:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
assert activity.actor == old_user.ap_id
assert activity.data["actor"] == old_user.ap_id
assert activity.data["object"] == old_user.ap_id
assert activity.data["target"] == new_user.ap_id
assert activity.data["type"] == "Move"
end
test "it fixes both the Create and object contexts in a reply" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
insert(:user, ap_id: "https://p.helene.moe/users/helene")
create_activity =
"test/fixtures/create-pleroma-reply-to-misskey-thread.json"
|> File.read!()
|> Jason.decode!()
assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(create_activity)
object = Object.normalize(activity, fetch: false)
assert activity.data["context"] == object.data["context"]
end
test "it fixes the public scope addressing" do
insert(:user,
ap_id: "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0"
)
object =
"test/fixtures/bovine-bogus-public-note.json"
|> File.read!()
|> Jason.decode!()
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0",
"object" => object
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity, fetch: false)
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
end
test "it keeps link tags" do
insert(:user, ap_id: "https://example.org/users/alice")
message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!()
assert capture_log(fn ->
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity)
assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"]
end) =~ "Object rejected while fetching"
end
test "it accepts quote posts" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => "https://misskey.io/users/7rkrarq81i",
"object" => object
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
# Object was created in the database
object = Object.normalize(activity)
assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0"
# It fetched the quoted post
assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
end
test "doesn't allow remote edits to fake local likes" do
# as a spot check for no internal fields getting injected
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [local_user.ap_id]
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
end
test "strips internal fields from history items in edited notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [],
"like_count" => 0
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [
Map.merge(create_data["object"], %{
"likes" => [local_user.ap_id],
"like_count" => 1,
"pleroma" => %{"internal_field" => "should_be_stripped"}
})
]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
# Check that internal fields are stripped from history items
history_item = List.first(object.data["formerRepresentations"]["orderedItems"])
assert history_item["likes"] == []
assert history_item["like_count"] == 0
refute Map.has_key?(history_item, "pleroma")
end
test "doesn't trip over remote likes in notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
test "doesn't trip over remote likes in polls" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Question",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "vote!",
"anyOf" => [
%{
"type" => "Note",
"name" => "a",
"replies" => %{
"type" => "Collection",
"totalItems" => 3
}
},
%{
"type" => "Note",
"name" => "b",
"replies" => %{
"type" => "Collection",
"totalItems" => 1
}
}
],
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "vote now!")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "vote!"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "vote now!"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
end
describe "prepare outgoing" do
test "it inlines private announced objects" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"})
{:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
{:ok, modified} = Transmogrifier.prepare_activity(announce_activity.data)
assert modified["object"]["content"] == "hey"
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"})
with_mock Pleroma.Notification,
get_notified_from_activity: fn _, _ -> [] end do
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
refute called(Pleroma.Notification.get_notified_from_activity(:_, :_))
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["@context"] == Utils.make_json_ld_header()["@context"]
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it strips internal hashtag data" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu"})
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["tag"] == [expected_tag]
end
test "it strips internal fields" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "#2hu :firefox:",
generator: %{type: "Application", name: "TestClient", url: "https://pleroma.social"}
})
# Ensure injected application data made it into the activity
# as we don't have a Token to derive it from, otherwise it will
# be nil and the test will pass
assert %{
type: "Application",
name: "TestClient",
url: "https://pleroma.social"
} == activity.object.data["generator"]
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["generator"])
assert is_nil(modified["object"]["rules"])
assert is_nil(modified["object"]["language"])
assert is_nil(modified["object"]["voters"])
end
test "it strips internal fields of article" do
activity = insert(:article_activity)
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["likes"])
end
test "the directMessage flag is present" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
{:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} :moominmamma:",
visibility: "direct"
})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == true
end
test "it strips BCC field" do
user = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert is_nil(modified["bcc"])
end
test "it can handle Listen activities" do
listen_activity = insert(:listen)
# This has an inlined object as in ObjectView
{:ok, modified} = Transmogrifier.prepare_activity(listen_activity.data)
assert modified["type"] == "Listen"
user = insert(:user)
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
user_ap_id = user.ap_id
activity_ap_id = activity.data["id"]
activity_to = activity.data["to"]
activity_cc = activity.data["cc"]
object_ap_id = activity.data["object"]
object_type = activity.object.data["type"]
# This does not have an inlined object
{:ok, modified2} = Transmogrifier.prepare_activity(activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "Listen",
"actor" => ^user_ap_id,
"to" => ^activity_to,
"cc" => ^activity_cc,
"context" => "http://localhost" <> _,
"id" => ^activity_ap_id,
"object" => %{
"actor" => ^user_ap_id,
"attributedTo" => ^user_ap_id,
"id" => ^object_ap_id,
"type" => ^object_type,
"to" => ^activity_to,
"cc" => ^activity_cc
}
},
modified2
)
end
test "custom emoji urls are URI encoded" do
# :dinosaur: filename has a space -> dino walking.gif
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
{:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert length(prepared["object"]["tag"]) == 1
url = prepared["object"]["tag"] |> List.first() |> Map.get("icon") |> Map.get("url")
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "Updates of Notes are handled" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
{:ok, update} = CommonAPI.update(activity, user, %{status: "mew mew :blank:"})
{:ok, prepared} = Transmogrifier.prepare_activity(update.data)
assert %{
"content" => "mew mew :blank:",
"tag" => [%{"name" => ":blank:", "type" => "Emoji"}],
"formerRepresentations" => %{
"orderedItems" => [
%{
"content" => "everybody do the dinosaur :dinosaur:",
"tag" => [%{"name" => ":dinosaur:", "type" => "Emoji"}]
}
]
}
} = prepared["object"]
end
test "Updates of Actors are handled" do
user = insert(:user, local: true)
changeset = User.update_changeset(user, %{name: "new name"})
{:ok, unpersisted_user} = Ecto.Changeset.apply_action(changeset, :update)
updated_object =
UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context")
{:ok, update_data, []} = Builder.update(user, updated_object)
{:ok, activity, _} =
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
)
assert {:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert prepared["type"] == "Update"
assert prepared["@context"]
assert prepared["object"]["type"] == user.actor_type
end
test "Correctly handles Undo activities" do
blocked = insert(:user)
blocker = insert(:user, local: true)
blocked_ap_id = blocked.ap_id
blocker_ap_id = blocker.ap_id
{:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker)
{:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker)
{:ok, data} = Transmogrifier.prepare_activity(undo_activity.data)
block_ap_id = block_activity.data["id"]
assert is_binary(block_ap_id)
assert match?(
%{
"@context" => [_ | _],
"type" => "Undo",
"id" => "http://localhost" <> _,
"actor" => ^blocker_ap_id,
"object" => ^block_ap_id,
"to" => [^blocked_ap_id],
"cc" => [],
"bto" => [],
"bcc" => []
},
data
)
end
test "Correctly handles EmojiReact activities" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
user_ap_id = user.ap_id
user_followers = user.follower_address
note_author = note_activity.data["actor"]
note_ap_id = note_activity.data["object"]
assert is_binary(note_author)
assert is_binary(note_ap_id)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈")
{:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "EmojiReact",
"actor" => ^user_ap_id,
"to" => [^user_followers, ^note_author],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"bto" => [],
"bcc" => [],
"content" => "🐈",
"context" => "2hu",
"id" => "http://localhost" <> _,
"object" => ^note_ap_id,
"tag" => []
},
data
)
end
test "EmojiReact custom emoji urls are URI encoded" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:")
{:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert length(data["tag"]) == 1
tag = List.first(data["tag"])
url = tag["icon"]["url"]
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
assert tag["id"] == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "it prepares a quote post" do
user = insert(:user)
{:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"})
{:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quoted_status_id: quoted_post.id})
{:ok, modified} = Transmogrifier.prepare_activity(quote_post.data)
%{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
end
test "it adds language of the object to its json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.object.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it adds language of the object to Create activity json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it strips report data" do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", reporter.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
test "it strips report data and anonymize" do
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", placeholder.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
end
describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{
"url" => %{"href" => "http://example.com"}
}
rewritten = Transmogrifier.maybe_fix_user_object(data)
assert rewritten["url"] == "http://example.com"
end
end
describe "actor origin containment" do
test "it rejects activities which reference objects with bogus origins" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity2.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity3.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
end
describe "fix_explicit_addressing" do
setup do
user = insert(:user)
[user: user]
end
test "moves non-explicitly mentioned actors to cc", %{user: user} do
explicitly_mentioned_actors = [
"https://pleroma.gold/users/user1",
"https://pleroma.gold/user2"
]
object = %{
"actor" => user.ap_id,
"to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
"cc" => [],
"tag" =>
Enum.map(explicitly_mentioned_actors, fn href ->
%{"type" => "Mention", "href" => href}
end)
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
end
test "does not move actor's follower collection to cc", %{user: user} do
object = %{
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => []
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert user.follower_address in fixed_object["to"]
refute user.follower_address in fixed_object["cc"]
end
test "removes recipient's follower collection from cc", %{user: user} do
recipient = insert(:user)
object = %{
"actor" => user.ap_id,
"to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address, recipient.follower_address]
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert user.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["to"]
end
end
describe "fix_summary/1" do
test "returns fixed object" do
assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
end
end
describe "fix_url/1" do
test "fixes data for object when url is map" do
object = %{
"url" => %{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
}
assert Transmogrifier.fix_url(object) == %{
"url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
end
test "returns non-modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
describe "get_obj_helper/2" do
test "returns nil when cannot normalize object" do
assert capture_log(fn ->
refute Transmogrifier.get_obj_helper("test-obj-id")
end) =~ "Unsupported URI scheme"
end
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper(
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
)
end
end
describe "fix_attachments/1" do
test "puts dimensions into attachment url field" do
object = %{
"attachment" => [
%{
"type" => "Document",
"name" => "Hello world",
"url" => "https://media.example.tld/1.jpg",
"width" => 880,
"height" => 960,
"mediaType" => "image/jpeg",
"blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
}
]
}
expected = %{
"attachment" => [
%{
"type" => "Document",
"name" => "Hello world",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpeg",
"href" => "https://media.example.tld/1.jpg",
"width" => 880,
"height" => 960
}
],
"mediaType" => "image/jpeg",
"blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
}
]
}
assert Transmogrifier.fix_attachments(object) == expected
end
end
describe "prepare_object/1" do
test "it processes history" do
original = %{
"formerRepresentations" => %{
"orderedItems" => [
%{
"generator" => %{},
"emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"}
}
]
}
}
processed = Transmogrifier.prepare_object(original)
history_item = Enum.at(processed["formerRepresentations"]["orderedItems"], 0)
refute Map.has_key?(history_item, "generator")
assert [%{"name" => ":blobcat:"}] = history_item["tag"]
end
test "it works when there is no or bad history" do
original = %{
"formerRepresentations" => %{
"items" => [
%{
"generator" => %{},
"emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"}
}
]
}
}
processed = Transmogrifier.prepare_object(original)
assert processed["formerRepresentations"] == original["formerRepresentations"]
end
test "it uses contentMap to specify post language" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
object = Transmogrifier.prepare_object(activity.object.data)
assert %{"contentMap" => %{"pl" => "Cześć"}} = object
end
end
end
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index 3b77f0867..93234a015 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -1,760 +1,762 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UtilsTest do
use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
require Pleroma.Constants
describe "strip_report_status_data/1" do
test "does not break on issues with the reported activities" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"})
context = Utils.generate_context_id()
content = "foobar"
post_id = activity.data["id"]
res =
Utils.make_flag_data(
%{
actor: reporter,
context: context,
account: target_account,
statuses: [%{"id" => post_id}],
content: content
},
%{}
)
res =
res
|> Map.put("object", res["object"] ++ [nil, 1, 5, "123"])
{:ok, activity} = Pleroma.Web.ActivityPub.ActivityPub.insert(res)
[user_id, object | _] = activity.data["object"]
{:ok, stripped} = Utils.strip_report_status_data(activity)
assert stripped.data["object"] == [user_id, object["id"]]
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = User.get_cached_by_ap_id(activity.data["actor"])
followed = User.get_cached_by_ap_id(activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "determine_explicit_mentions()" do
test "works with an object that has mentions" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that does not have mentions" do
object = %{
"tag" => [
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has mentions and other tags" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
},
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that has no tags" do
object = %{}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has only IR tags" do
object = %{"tag" => ["2hu"]}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object has tags as map" do
object = %{
"tag" => %{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
}
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
end
describe "make_like_data" do
setup do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
[user: user, other_user: other_user, third_user: third_user]
end
test "addresses actor's follower address if the activity is public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id, other_user.follower_address])
expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id])
{:ok, activity} =
CommonAPI.post(user, %{
status:
"hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
test "does not address actor's follower address if the activity is not public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id])
expected_cc = [third_user.ap_id]
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!",
visibility: "private"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
end
describe "make_json_ld_header/1" do
test "makes jsonld header" do
assert Utils.make_json_ld_header() == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "und"
+ "@language" => "und",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
end
test "includes language if specified" do
assert Utils.make_json_ld_header(%{"language" => "pl"}) == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "pl"
+ "@language" => "pl",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
end
end
describe "get_existing_votes" do
test "fetches existing votes" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "How do I pronounce LaTeX?",
poll: %{
options: ["laytekh", "lahtekh", "latex"],
expires_in: 20,
multiple: true
}
})
object = Object.normalize(activity, fetch: false)
{:ok, votes, object} = CommonAPI.vote(object, other_user, [0, 1])
assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes)
end
test "fetches only Create activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Are we living in a society?",
poll: %{
options: ["yes", "no"],
expires_in: 20
}
})
object = Object.normalize(activity, fetch: false)
{:ok, [vote], object} = CommonAPI.vote(object, other_user, [0])
{:ok, _activity} = CommonAPI.favorite(activity.id, user)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id
end
end
describe "update_follow_state_for_all/2" do
test "updates the state of all Follow activities with the same actor and object" do
user = insert(:user, is_locked: true)
follower = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(user, follower)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "accept")
assert refresh_record(follow_activity).data["state"] == "accept"
assert refresh_record(follow_activity_two).data["state"] == "accept"
end
test "also updates the state of accepted follows" do
user = insert(:user)
follower = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(user, follower)
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "reject")
assert refresh_record(follow_activity).data["state"] == "reject"
assert refresh_record(follow_activity_two).data["state"] == "reject"
end
end
describe "update_follow_state/2" do
test "updates the state of the given follow activity" do
user = insert(:user, is_locked: true)
follower = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(user, follower)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject")
assert refresh_record(follow_activity).data["state"] == "pending"
assert refresh_record(follow_activity_two).data["state"] == "reject"
end
end
describe "update_element_in_object/3" do
test "updates likes" do
user = insert(:user)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
assert {:ok, updated_object} =
Utils.update_element_in_object(
"like",
[user.ap_id],
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "add_like_to_object/2" do
test "add actor to likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note)
assert {:ok, updated_object} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
assert {:ok, updated_object2} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user2.ap_id}},
updated_object
)
assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id]
assert updated_object2.data["like_count"] == 2
end
end
describe "remove_like_from_object/2" do
test "removes ap_id from likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2})
assert {:ok, updated_object} =
Utils.remove_like_from_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user2.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "get_existing_like/2" do
test "fetches existing like" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity, fetch: false)
user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object)
{:ok, like_activity} = CommonAPI.favorite(note_activity.id, user)
assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end
end
describe "get_get_existing_announce/2" do
test "returns nil if announce not found" do
actor = insert(:user)
refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}})
end
test "fetches existing announce" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity, fetch: false)
actor = insert(:user)
{:ok, announce} = CommonAPI.repeat(note_activity.id, actor)
assert Utils.get_existing_announce(actor.ap_id, object) == announce
end
end
describe "fetch_latest_block/2" do
test "fetches last block activities" do
user1 = insert(:user)
user2 = insert(:user)
assert {:ok, %Activity{} = _} = CommonAPI.block(user2, user1)
assert {:ok, %Activity{} = _} = CommonAPI.block(user2, user1)
assert {:ok, %Activity{} = activity} = CommonAPI.block(user2, user1)
assert Utils.fetch_latest_block(user1, user2) == activity
end
end
describe "recipient_in_message/3" do
test "returns true when recipient in `to`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"to" => [recipient.ap_id], "cc" => ""}
)
end
test "returns true when recipient in `cc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"cc" => [recipient.ap_id], "to" => ""}
)
end
test "returns true when recipient in `bto`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bcc" => "", "bto" => [recipient.ap_id]}
)
end
test "returns true when recipient in `bcc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bto" => "", "bcc" => [recipient.ap_id]}
)
end
test "returns true when message without addresses fields" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"btod" => "", "bccc" => [recipient.ap_id]}
)
end
test "returns false" do
recipient = insert(:user)
actor = insert(:user)
refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
end
end
describe "lazy_put_activity_defaults/2" do
test "returns map with id and published data" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity, fetch: false)
res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]})
assert res["context"] == object.data["id"]
assert res["id"]
assert res["published"]
end
test "returns map with fake id and published data" do
assert %{
"context" => "pleroma:fakecontext",
"id" => "pleroma:fakeid",
"published" => _
} = Utils.lazy_put_activity_defaults(%{}, true)
end
test "returns activity data with object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity, fetch: false)
res =
Utils.lazy_put_activity_defaults(%{
"context" => object.data["id"],
"object" => %{}
})
assert res["context"] == object.data["id"]
assert res["id"]
assert res["published"]
assert res["object"]["id"]
assert res["object"]["published"]
assert res["object"]["context"] == object.data["id"]
end
end
describe "make_flag_data" do
test "returns empty map when params is invalid" do
assert Utils.make_flag_data(%{}, %{}) == %{}
end
test "returns map with Flag object" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"})
context = Utils.generate_context_id()
content = "foobar"
target_ap_id = target_account.ap_id
object_ap_id = activity.object.data["id"]
res =
Utils.make_flag_data(
%{
actor: reporter,
context: context,
account: target_account,
statuses: [%{"id" => activity.data["id"]}],
content: content
},
%{}
)
note_obj = %{
"type" => "Note",
"id" => object_ap_id,
"content" => content,
"published" => activity.object.data["published"],
"actor" =>
AccountView.render("show.json", %{user: target_account, skip_visibility_check: true})
}
assert %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
} = res
end
test "returns map with Flag object with a non-Create Activity" do
reporter = insert(:user)
posting_account = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(posting_account, %{status: "foobar"})
{:ok, like} = CommonAPI.favorite(activity.id, target_account)
context = Utils.generate_context_id()
content = "foobar"
target_ap_id = target_account.ap_id
object_ap_id = activity.object.data["id"]
res =
Utils.make_flag_data(
%{
actor: reporter,
context: context,
account: target_account,
statuses: [%{"id" => like.data["id"]}],
content: content
},
%{}
)
note_obj = %{
"type" => "Note",
"id" => object_ap_id,
"content" => content,
"published" => activity.object.data["published"],
"actor" =>
AccountView.render("show.json", %{user: posting_account, skip_visibility_check: true})
}
assert %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
} = res
end
end
describe "add_announce_to_object/2" do
test "adds actor to announcement" do
user = insert(:user)
object = insert(:note)
activity =
insert(:note_activity,
data: %{
"actor" => user.ap_id,
"cc" => [Pleroma.Constants.as_public()]
}
)
assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object)
assert updated_object.data["announcements"] == [user.ap_id]
assert updated_object.data["announcement_count"] == 1
end
end
describe "remove_announce_from_object/2" do
test "removes actor from announcements" do
user = insert(:user)
user2 = insert(:user)
object =
insert(:note,
data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2}
)
activity = insert(:note_activity, data: %{"actor" => user.ap_id})
assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object)
assert updated_object.data["announcements"] == [user2.ap_id]
assert updated_object.data["announcement_count"] == 1
end
end
describe "get_cached_emoji_reactions/1" do
test "returns the normalized data or an empty list" do
object = insert(:note)
assert Utils.get_cached_emoji_reactions(object) == []
object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]})
assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"], nil]]
object = insert(:note, data: %{"reactions" => %{}})
assert Utils.get_cached_emoji_reactions(object) == []
end
end
describe "add_emoji_reaction_to_object/1" do
test "works with legacy 2-tuple format" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
note =
insert(:note,
user: user,
data: %{
"reactions" => [["😿", [other_user.ap_id]]]
}
)
_activity = insert(:note_activity, user: user, note: note)
Utils.add_emoji_reaction_to_object(
%Activity{data: %{"content" => "😿", "actor" => third_user.ap_id}},
note
)
end
end
describe "assign_report_to_account/2" do
test "assigns report to an account" do
reporter = insert(:user)
target_account = insert(:user)
%{id: assigned_id} = insert(:user)
{:ok, report} = CommonAPI.report(reporter, %{account_id: target_account.id})
{:ok, report} = Utils.assign_report_to_account(report, assigned_id)
assert %{data: %{"assigned_account" => ^assigned_id}} = report
end
end
describe "maybe_anonymize_reporter/1" do
setup do
reporter = insert(:user)
report = %{"actor" => reporter.ap_id}
%{
placeholder: insert(:user),
reporter: reporter,
report: report
}
end
test "anonymize when configured correctly", %{
placeholder: placeholder,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => placeholder.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "anonymize Activity", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
report_activity = %Activity{actor: reporter, data: report}
anon_id = placeholder.ap_id
assert %Activity{actor: ^anon_id, data: %{"actor" => ^anon_id}} =
Utils.maybe_anonymize_reporter(report_activity)
end
test "do not anonymize when disabled", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], false)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user does not exist", %{
placeholder: placeholder,
reporter: reporter,
report: report
} do
clear_config([:activitypub, :anonymize_reporter], true)
clear_config(
[:activitypub, :anonymize_reporter_local_nickname],
placeholder.nickname <> "MewMew"
)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
test "do not anonymize when user is not local", %{
reporter: reporter,
report: report
} do
placeholder = insert(:user, local: false)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
assert %{"actor" => reporter.ap_id} == Utils.maybe_anonymize_reporter(report)
end
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 017fac696..5d0ad4572 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -1,2226 +1,2267 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.Tests.ObanHelpers
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.PollWorker
import Ecto.Query, only: [from: 2, where: 3]
import Mock
import Mox
import Pleroma.Factory
require Pleroma.Activity.Queries
require Pleroma.Constants
defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
Pleroma.Activity.Queries.by_type("Announce")
|> Pleroma.Activity.Queries.by_object_id(id)
|> Pleroma.Repo.all()
end
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
:ok
end
setup do: clear_config([:instance, :safe_dm_mentions])
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])
describe "posting polls" do
test "it posts a poll" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "who is the best",
poll: %{expires_in: 600, options: ["reimu", "marisa"]}
})
object = Object.normalize(activity, fetch: false)
assert object.data["type"] == "Question"
assert object.data["oneOf"] |> length() == 2
assert_enqueued(
worker: PollWorker,
args: %{op: "poll_end", activity_id: activity.id},
scheduled_at: NaiveDateTime.from_iso8601!(object.data["closed"])
)
end
end
describe "blocking" do
setup do
blocker = insert(:user)
blocked = insert(:user, local: false)
CommonAPI.follow(blocked, blocker)
CommonAPI.follow(blocker, blocked)
CommonAPI.accept_follow_request(blocker, blocked)
CommonAPI.accept_follow_request(blocked, blocked)
%{blocker: blocker, blocked: blocked}
end
test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert User.get_follow_state(blocker, blocked) == :follow_accept
refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked))
assert {:ok, block} = CommonAPI.block(blocked, blocker)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute User.get_follow_state(blocker, blocked)
assert %{data: %{"state" => "reject"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)
assert called(Pleroma.Web.Federator.publish(block))
end
end
test "add expiring block", %{blocker: blocker, blocked: blocked} do
{:ok, _} = CommonAPI.block(blocked, blocker, %{expires_in: 60})
assert User.blocks?(blocker, blocked)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id}
assert :ok = perform_job(worker, args)
refute User.blocks?(blocker, blocked)
end
test "it blocks and does not federate if outgoing blocks are disabled", %{
blocker: blocker,
blocked: blocked
} do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocked, blocker)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute called(Pleroma.Web.Federator.publish(block))
end
end
end
describe "posting chat messages" do
setup do: clear_config([:instance, :chat_limit])
test "it posts a self-chat" do
author = insert(:user)
recipient = author
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"remember to buy milk when milk truk arive"
)
assert activity.data["type"] == "Create"
end
test "it posts a chat message without content but with an attachment" do
author = insert(:user)
recipient = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ ->
nil
end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
nil,
media_id: upload.id
)
notification =
Notification.for_user_and_activity(recipient, activity)
|> Repo.preload(:activity)
assert called(Pleroma.Web.Push.send(notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
assert activity
end
end
test "it adds html newlines" do
author = insert(:user)
recipient = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"uguu\nuguuu"
)
assert other_user.ap_id not in activity.recipients
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "uguu<br/>uguuu"
end
test "it linkifies" do
author = insert(:user)
recipient = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"https://example.org is the site of @#{other_user.nickname} #2hu"
)
assert other_user.ap_id not in activity.recipients
object = Object.normalize(activity, fetch: false)
assert object.data["content"] ==
"<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{other_user.id}\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
end
test "it posts a chat message" do
author = insert(:user)
recipient = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"a test message <script>alert('uuu')</script> :firefox:"
)
assert activity.data["type"] == "Create"
assert activity.local
object = Object.normalize(activity, fetch: false)
assert object.data["type"] == "ChatMessage"
assert object.data["to"] == [recipient.ap_id]
assert object.data["content"] ==
"a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
assert object.data["emoji"] == %{
"firefox" => "http://localhost:4001/emoji/Firefox.gif"
}
assert Chat.get(author.id, recipient.ap_id)
assert Chat.get(recipient.id, author.ap_id)
assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
end
test "it reject messages over the local limit" do
clear_config([:instance, :chat_limit], 2)
author = insert(:user)
recipient = insert(:user)
{:error, message} =
CommonAPI.post_chat_message(
author,
recipient,
"123"
)
assert message == :content_too_long
end
test "it reject messages via MRF" do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
author = insert(:user)
recipient = insert(:user)
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
end
test "it reject messages with attachments not belonging to user" do
author = insert(:user)
not_author = insert(:user)
recipient = author
attachment = insert(:attachment, %{user: not_author})
{:error, message} =
CommonAPI.post_chat_message(
author,
recipient,
"123",
media_id: attachment.id
)
assert message == :forbidden
end
end
describe "unblocking" do
test "it works even without an existing block activity" do
blocked = insert(:user)
blocker = insert(:user)
User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
assert {:ok, :no_activity} == CommonAPI.unblock(blocked, blocker)
refute User.blocks?(blocker, blocked)
end
end
describe "deletion" do
test "it works with pruned objects" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
Object.normalize(post, fetch: false)
|> Object.prune()
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it allows users to delete their posts" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it does not allow a user to delete posts from another user" do
user = insert(:user)
other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
assert Activity.get_by_id(post.id)
end
test "it allows privileged users to delete other user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "it doesn't allow unprivileged mods or admins to delete other user's posts" do
clear_config([:instance, :admin_privileges], [])
clear_config([:instance, :moderator_privileges], [])
user = insert(:user)
moderator = insert(:user, is_moderator: true, is_admin: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, moderator)
assert Activity.get_by_id(post.id)
end
test "privileged users deleting non-local posts won't federate the delete" do
clear_config([:instance, :admin_privileges], [:messages_delete])
# This is the user of the ingested activity
_user =
insert(:user,
local: false,
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: NaiveDateTime.utc_now()
)
admin = insert(:user, is_admin: true)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
{:ok, post} = Transmogrifier.handle_incoming(data)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, admin)
assert delete.local
refute called(Pleroma.Web.Federator.publish(:_))
end
refute Activity.get_by_id(post.id)
end
test "it allows privileged users to delete banned user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
User.set_activation(user, false)
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
end
test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.favorite(activity.id, user)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.favorite(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 20
end
test "repeating race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.repeat(activity.id, user)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.repeat(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 20
end
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
[participation] = Participation.for_user(user)
{:ok, convo_reply} =
CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id})
assert Visibility.direct?(convo_reply)
assert activity.data["context"] == convo_reply.data["context"]
end
test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
{:ok, activity} =
CommonAPI.post(har, %{
status: "@#{jafnhar.nickname} hey",
visibility: "direct"
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
[participation] = Participation.for_user(har)
{:ok, activity} =
CommonAPI.post(har, %{
status: "I don't really like @#{tridi.nickname}",
visibility: "direct",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
refute tridi.ap_id in activity.recipients
end
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
clear_config([:instance, :safe_dm_mentions], true)
{:ok, activity} =
CommonAPI.post(har, %{
status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
visibility: "direct"
})
refute tridi.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
end
test "it de-duplicates tags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"})
object = Object.normalize(activity, fetch: false)
assert Object.tags(object) == ["2hu"]
end
test "zwnj is treated as word character" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#ساٴين‌س"})
object = Object.normalize(activity, fetch: false)
assert Object.tags(object) == ["ساٴين‌س"]
end
test "allows lang attribute" do
user = insert(:user)
text = ~s{<span lang="en">something</span><p lang="diaetuitech_rpyhpgc">random</p>}
{:ok, activity} = CommonAPI.post(user, %{status: text, content_type: "text/html"})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == text
end
test "double dot in link is allowed" do
user = insert(:user)
text = "https://example.to/something..mp3"
{:ok, activity} = CommonAPI.post(user, %{status: text})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
end
test "it adds emoji in the object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"]
end
describe "posting" do
test "it adds an emoji on an external site" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"})
assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"]
assert url == "https://example.com/emoji.png"
{:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"})
assert %{"blank" => url} = Object.normalize(activity).data["emoji"]
assert url == "#{Pleroma.Web.Endpoint.url()}/emoji/blank.png"
end
test "it copies emoji from the subject of the parent post" do
%Object{} =
object =
Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f",
fetch: true
)
activity = Activity.get_create_by_object_ap_id(object.data["id"])
user = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(user, %{
in_reply_to_id: activity.id,
status: ":joker_disapprove:",
spoiler_text: ":joker_smile:"
})
assert Object.normalize(reply_activity).data["emoji"]["joker_smile"]
refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"]
end
test "deactivated users can't post" do
user = insert(:user, is_active: false)
assert {:error, _} = CommonAPI.post(user, %{status: "ye"})
end
test "it supports explicit addressing" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
user_four = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
to: [user_two.nickname, user_four.nickname, "nonexistent"]
})
assert user.ap_id in activity.recipients
assert user_two.ap_id in activity.recipients
assert user_four.ap_id in activity.recipients
refute user_three.ap_id in activity.recipients
end
test "it filters out obviously bad tags when accepting a post as HTML" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/html"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
assert object.data["source"]["content"] == post
end
test "it filters out obviously bad tags when accepting a post as Markdown" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/markdown"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>"
assert object.data["source"]["content"] == post
end
+ test "it renders MFM posts and marks their ActivityPub representation" do
+ user = insert(:user)
+
+ post = "<p class='scrub-this'>$[spin.speed=1s 13:37]</p>"
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: post,
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert object.data["htmlMfm"] == true
+
+ assert object.data["source"] == %{
+ "content" => post,
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+
+ assert object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ ~s(data-mfm-speed="1s")
+ assert object.data["content"] =~ "13:37"
+ refute object.data["content"] =~ "scrub-this"
+ end
+
+ test "it falls back safely for malformed MFM" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "$[spin malformed",
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ refute object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ "malformed"
+ end
+
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
assert {:ok, _} =
CommonAPI.post(user, %{
status: "suya..",
visibility: "direct",
in_reply_to_status_id: activity.id
})
Enum.each(["public", "private", "unlisted"], fn visibility ->
assert {:error, "The message visibility must be direct"} =
CommonAPI.post(user, %{
status: "suya..",
visibility: visibility,
in_reply_to_status_id: activity.id
})
end)
end
test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"})
{:ok, open_answer} =
CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id})
# The OP is implicitly added
assert user.ap_id in open_answer.recipients
{:ok, secret_answer} =
CommonAPI.post(other_user, %{
status: "lol, that guy really is stupid, right, @#{third_user.nickname}?",
in_reply_to_status_id: post.id,
visibility: "direct"
})
assert third_user.ap_id in secret_answer.recipients
# The OP is not added
refute user.ap_id in secret_answer.recipients
end
test "it allows to address a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
assert activity.data["bcc"] == [list.ap_id]
assert activity.recipients == [list.ap_id, user.ap_id]
assert activity.data["listMessage"] == list.ap_id
end
test "it returns error when status is empty and no attachments" do
user = insert(:user)
assert {:error, "Cannot post an empty status without attachments"} =
CommonAPI.post(user, %{status: ""})
end
test "it validates character limits are correctly enforced" do
clear_config([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{status: "foobar"})
assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
end
test "it validates media attachment limits are correctly enforced" do
clear_config([:instance, :max_media_attachments], 4)
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
assert {:error, "Too many attachments"} =
CommonAPI.post(user, %{
status: "",
media_ids: List.duplicate(upload.id, 5)
})
assert {:ok, _activity} =
CommonAPI.post(user, %{
status: "",
media_ids: [upload.id]
})
end
test "it can handle activities that expire" do
user = insert(:user)
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000})
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: activity.id},
scheduled_at: expires_at
)
end
test "it allows quote posting" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
{:ok, quote_post} =
CommonAPI.post(user, %{status: "nice post", quoted_status_id: quoted.id})
quoted = Object.normalize(quoted)
quote_post = Object.normalize(quote_post)
assert quote_post.data["quoteUrl"] == quoted.data["id"]
# The OP is not mentioned
refute quoted.data["actor"] in quote_post.data["to"]
end
test "it supports fallback from `quote_id`" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
{:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id})
quoted = Object.normalize(quoted)
quote_post = Object.normalize(quote_post)
assert quote_post.data["quoteUrl"] == quoted.data["id"]
end
test "quote posting with explicit addressing doesn't mention the OP" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
{:ok, quote_post} =
CommonAPI.post(user, %{status: "nice post", quoted_status_id: quoted.id, to: []})
assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
end
test "quote posting visibility" do
user = insert(:user)
another_user = insert(:user)
{:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
{:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
{:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
{:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
{:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:error, _} = CommonAPI.post(user, %{status: "nice", quoted_status_id: direct.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quoted_status_id: private.id})
{:error, _} = CommonAPI.post(another_user, %{status: "nice", quoted_status_id: private.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quoted_status_id: unlisted.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quoted_status_id: unlisted.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quoted_status_id: local.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quoted_status_id: local.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quoted_status_id: public.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quoted_status_id: public.id})
end
test "it properly mentions punycode domain" do
user = insert(:user)
_mentioned_user =
insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com")
{:ok, activity} =
CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"})
assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"]
end
end
describe "reactions" do
test "reacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".")
end
test "unreacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
{:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"]
assert unreaction.local
# On federation, it contains the undone (and deleted) object
unreaction_with_object = %{
unreaction
| data: Map.put(unreaction.data, "object", reaction.data)
}
assert called(Pleroma.Web.Federator.publish(unreaction_with_object))
end
end
test "repeating a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
assert Visibility.public?(announce_activity)
end
test "can't repeat a repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
end
test "repeating a status privately" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} =
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
assert Visibility.private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
end
test "author can repeat own private statuses" do
author = insert(:user)
follower = insert(:user)
CommonAPI.follow(author, follower)
{:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
assert Visibility.private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
assert Visibility.visible_for_user?(activity, follower)
assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower)
end
test "favoriting a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{data: data}} = CommonAPI.favorite(post_activity.id, user)
assert data["type"] == "Like"
assert data["actor"] == user.ap_id
assert data["object"] == post_activity.data["object"]
end
test "retweeting a status twice returns the status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
{:ok, ^announce} = CommonAPI.repeat(activity.id, user)
end
test "favoriting a status twice returns ok, but without the like activity" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{}} = CommonAPI.favorite(activity.id, user)
assert {:ok, :already_liked} = CommonAPI.favorite(activity.id, user)
end
end
describe "pinned statuses" do
setup do
clear_config([:instance, :max_pinned_statuses], 1)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
[user: user, activity: activity]
end
test "activity not found error", %{user: user} do
assert {:error, :not_found} = CommonAPI.pin("id", user)
end
test "pin status", %{user: user, activity: activity} do
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "pin poll", %{user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "How is fediverse today?",
poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20}
})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "unlisted statuses can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
end
test "only self-authored can be pinned", %{activity: activity} do
user = insert(:user)
assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user)
end
test "max pinned statuses", %{user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user)
end
test "only public can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
{:error, :non_public_error} = CommonAPI.pin(activity.id, user)
end
test "unpin status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id = activity.id
assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "should unpin when deleting a status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
assert {:ok, _} = CommonAPI.delete(activity.id, user)
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "ephemeral activity won't be deleted if was pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
user = refresh_record(user)
{:ok, _} = CommonAPI.unpin(activity.id, user)
# recreates expiration job on unpin
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
end
test "ephemeral activity deletion job won't be deleted on pinning error", %{
user: user,
activity: activity
} do
clear_config([:instance, :max_pinned_statuses], 1)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
{:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
user = refresh_record(user)
{:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user)
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
end
end
describe "mute tests" do
setup do
user = insert(:user)
activity = insert(:note_activity)
[user: user, activity: activity]
end
test "marks notifications as read after mute" do
author = insert(:user)
activity = insert(:note_activity, user: author)
friend1 = insert(:user)
friend2 = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(
friend2,
%{
status: "@#{author.nickname} @#{friend1.nickname} test reply",
in_reply_to_status_id: activity.id
}
)
{:ok, favorite_activity} = CommonAPI.favorite(activity.id, friend2)
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
unread_notifications =
Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id))
assert Enum.any?(unread_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
{:ok, _} = CommonAPI.add_mute(activity, author)
assert CommonAPI.thread_muted?(activity, author)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
read_notifications =
Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id))
assert Enum.any?(read_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
end
test "add mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(activity, user)
assert CommonAPI.thread_muted?(activity, user)
end
test "add expiring mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(activity, user, %{expires_in: 60})
assert CommonAPI.thread_muted?(activity, user)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute CommonAPI.thread_muted?(activity, user)
end
test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:ok, _} = CommonAPI.remove_mute(activity, user)
refute CommonAPI.thread_muted?(activity, user)
end
test "remove mute by ids", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:ok, _} = CommonAPI.remove_mute(activity.id, user.id)
refute CommonAPI.thread_muted?(activity, user)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:error, _} = CommonAPI.add_mute(activity, user)
end
end
describe "reports" do
test "creates a report" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"})
activity = Activity.normalize(activity)
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
reported_object_ap_id = activity.object.data["id"]
comment = "foobar"
report_data = %{
account_id: target_user.id,
comment: comment,
status_ids: [activity.id]
}
note_obj = %{
"type" => "Note",
"id" => reported_object_ap_id,
"content" => "foobar",
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_user})
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^comment,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
}
} = flag_activity
end
test "doesn't create a report when post is not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, post} = CommonAPI.post(target_user, %{status: "Eric", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, reporter)
# Fails when all status are invisible
report_data = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [post.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data)
end
test "doesn't create a report when some posts are not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, visible_activity} = CommonAPI.post(target_user, %{status: "cofe"})
{:ok, invisibile_activity} =
CommonAPI.post(target_user, %{status: "cawfee", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisibile_activity)
assert Pleroma.Web.ActivityPub.Visibility.public?(visible_activity)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisibile_activity, reporter)
# Fails when some statuses are invisible
report_data_partial = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [visible_activity.id, invisibile_activity.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data_partial)
end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
object = Object.normalize(activity)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, object_id] = report.data["object"]
assert reported_user == target_user.ap_id
assert object_id == object.data["id"]
end
test "updates report state, don't strip when report_strip_status is false" do
clear_config([:instance, :report_strip_status], false)
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id, data: report_data}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, reported_activity] = report.data["object"]
assert reported_user == target_user.ap_id
assert is_map(reported_activity)
assert reported_activity["content"] ==
report_data["object"] |> Enum.at(1) |> Map.get("content")
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
test "updates state of multiple reports" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel very offended!",
status_ids: [activity.id]
})
{:ok, report_ids} =
CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
first_report = Activity.get_by_id(first_report_id)
second_report = Activity.get_by_id(second_report_id)
assert report_ids -- [first_report_id, second_report_id] == []
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
test "creates a report with provided rules" do
reporter = insert(:user)
target_user = insert(:user)
%{id: rule_id} = Rule.create(%{text: "There are no rules"})
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
report_data = %{
account_id: target_user.id,
rule_ids: [rule_id]
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"object" => [^target_ap_id],
"state" => "open",
"rules" => [^rule_id]
}
} = flag_activity
end
test "assigns report to an account" do
[reporter, target_user] = insert_pair(:user)
%{id: assigned} = insert(:user)
{:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id})
{:ok, activity} = CommonAPI.assign_report_to_account(report_id, assigned)
assert %{data: %{"assigned_account" => ^assigned}} = activity
end
test "unassigns report from account" do
[reporter, target_user] = insert_pair(:user)
%{id: assigned} = insert(:user)
{:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id})
CommonAPI.assign_report_to_account(report_id, assigned)
{:ok, activity} = CommonAPI.assign_report_to_account(report_id, nil)
refute Map.has_key?(activity.data, "assigned_account")
end
end
describe "reblog muting" do
setup do
muter = insert(:user)
muted = insert(:user)
[muter: muter, muted: muted]
end
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muted, muter)
assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muted, muter)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(muted, muter)
assert User.showing_reblogs?(muter, muted) == true
end
end
describe "follow/2" do
test "directly follows a non-locked local user" do
[follower, followed] = insert_pair(:user)
{:ok, followed, follower, _} = CommonAPI.follow(followed, follower)
assert User.following?(follower, followed)
end
end
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, followed, follower, _} = CommonAPI.follow(followed, follower)
{:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(followed, follower)
refute User.subscribed_to?(follower, followed)
end
test "also unpins a user" do
[follower, followed] = insert_pair(:user)
{:ok, followed, follower, _} = CommonAPI.follow(followed, follower)
{:ok, _endorsement} = User.endorse(follower, followed)
assert User.endorses?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(followed, follower)
refute User.endorses?(follower, followed)
end
test "cancels a pending follow for a local user" do
follower = insert(:user)
followed = insert(:user, is_locked: true)
assert {:ok, followed, follower, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(followed, follower)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(followed, follower)
assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
test "cancels a pending follow for a remote user" do
follower = insert(:user)
followed = insert(:user, is_locked: true, local: false)
assert {:ok, followed, follower, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(followed, follower)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(followed, follower)
assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
end
describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(user, follower_two)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "after rejection, it sets all existing pending follow request states to 'reject'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(user, follower)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(user, follower_two)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
user = insert(:user, is_locked: true)
not_follower = insert(:user)
CommonAPI.accept_follow_request(not_follower, user)
assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
end
end
describe "vote/3" do
test "does not allow to vote twice" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Am I cute?",
poll: %{options: ["Yes", "No"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
{:ok, _, object} = CommonAPI.vote(object, other_user, [0])
assert {:error, "Already voted"} == CommonAPI.vote(object, other_user, [1])
end
end
describe "listen/2" do
test "returns a valid activity" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
title: "lain radio episode 1",
album: "lain radio",
artist: "lain",
length: 180_000
})
object = Object.normalize(activity, fetch: false)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "public"
end
test "respects visibility=private" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
title: "lain radio episode 1",
album: "lain radio",
artist: "lain",
length: 180_000,
visibility: "private"
})
object = Object.normalize(activity, fetch: false)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "private"
end
end
describe "get_user/1" do
test "gets user by ap_id" do
user = insert(:user)
assert CommonAPI.get_user(user.ap_id) == user
end
test "gets user by guessed nickname" do
user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
end
test "fallback" do
assert %User{
name: "",
ap_id: "",
nickname: "erroruser@example.com"
} = CommonAPI.get_user("")
end
end
describe "with `local` visibility" do
setup do: clear_config([:instance, :federating], true)
test "post" do
user = insert(:user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
assert Visibility.local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "delete" do
user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
CommonAPI.delete(activity_id, user)
assert Visibility.local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
CommonAPI.repeat(activity_id, user)
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unrepeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
assert {:ok, _} = CommonAPI.repeat(activity_id, user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unrepeat(activity_id, user)
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "favorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
CommonAPI.favorite(activity.id, user)
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unfavorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, %Activity{}} = CommonAPI.favorite(activity.id, user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "react_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
CommonAPI.react_with_emoji(activity.id, user, "👍")
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unreact_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
end
describe "update/3" do
test "updates a post" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
{:ok, updated} = CommonAPI.update(activity, user, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
test "does not change visibility" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
{:ok, updated} = CommonAPI.update(activity, user, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Visibility.get_visibility(updated_object) == "private"
assert Visibility.get_visibility(updated) == "private"
end
test "updates a post with emoji" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
{:ok, updated} = CommonAPI.update(activity, user, %{status: "updated 2 :#{emoji2}:"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated_object.data["emoji"]
end
test "updates a post with emoji and federate properly" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _p -> nil end do
{:ok, updated} = CommonAPI.update(activity, user, %{status: "updated 2 :#{emoji2}:"})
assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated.data["object"]["emoji"]
assert called(Pleroma.Web.Federator.publish(updated))
end
end
test "editing a post that copied a remote title with remote emoji should keep that emoji" do
remote_emoji_uri = "https://remote.org/emoji.png"
note =
insert(
:note,
data: %{
"summary" => ":remoteemoji:",
"emoji" => %{
"remoteemoji" => remote_emoji_uri
},
"tag" => [
%{
"type" => "Emoji",
"name" => "remoteemoji",
"icon" => %{"url" => remote_emoji_uri}
}
]
}
)
note_activity = insert(:note_activity, note: note)
user = insert(:user)
{:ok, reply} =
CommonAPI.post(user, %{
status: "reply",
spoiler_text: ":remoteemoji:",
in_reply_to_id: note_activity.id
})
assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri
{:ok, edit} =
CommonAPI.update(reply, user, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"})
edited_note = Pleroma.Object.normalize(edit)
assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri
end
test "respects MRF" do
user = insert(:user)
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}])
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"})
assert Object.normalize(activity).data["summary"] == "mewmew 1"
{:ok, updated} = CommonAPI.update(activity, user, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "mewmew 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
end
describe "Group actors" do
setup do
poster = insert(:user)
group = insert(:user, actor_type: "Group")
other_group = insert(:user, actor_type: "Group")
%{poster: poster, group: group, other_group: other_group}
end
test "it boosts public posts", %{poster: poster, group: group} do
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_] = announces
end
test "it does not boost private posts", %{poster: poster, group: group} do
{:ok, private_post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname}", visibility: "private"})
assert [] = get_announces_of_object(private_post.object)
end
test "remote groups do not boost any posts", %{poster: poster} do
remote_group =
insert(:user, actor_type: "Group", local: false, nickname: "remote@example.com")
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{User.full_nickname(remote_group)}"})
assert remote_group.ap_id in post.data["to"]
announces = get_announces_of_object(post.object)
assert [] = announces
end
test "multiple groups mentioned", %{poster: poster, group: group, other_group: other_group} do
{:ok, post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname} @#{other_group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_, _] = announces
end
test "it does not boost if group is blocking poster", %{poster: poster, group: group} do
{:ok, _} = CommonAPI.block(poster, group)
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
announces = get_announces_of_object(post.object)
assert [] = announces
end
end
describe "Oban jobs are cancelled" do
setup do
clear_config([:instance, :federating], true)
local_user = insert(:user)
remote_one =
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
inbox: "https://domain.com/users/nick1/inbox",
shared_inbox: "https://domain.com/inbox"
})
remote_two =
insert(:user, %{
local: false,
nickname: "nick2@example.com",
ap_id: "https://example.com/users/nick2",
inbox: "https://example.com/users/nick2/inbox",
shared_inbox: "https://example.com/inbox"
})
%{local_user: local_user, remote_one: remote_one, remote_two: remote_two}
end
test "when deleting posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, %{id: activity_id} = _activity} =
CommonAPI.post(local_user, %{status: "Happy Friday everyone!"})
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"activity_id" => ^activity_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The delete should have triggered cancelling the publish_one jobs
assert {:ok, _delete} = CommonAPI.delete(activity_id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["activity_id"] == ^activity_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
test "when unfavoriting posts", %{
local_user: local_user,
remote_one: remote_user
} do
{:ok, activity} =
CommonAPI.post(remote_user, %{status: "I like turtles!"})
{:ok, %{id: favorite_id} = _favorite} =
CommonAPI.favorite(activity.id, local_user)
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"activity_id" => ^favorite_id}}
},
job
)
end)
assert length(publish_one_jobs) == 1
# The unfavorite should have triggered cancelling the publish_one jobs
assert {:ok, _unfavorite} = CommonAPI.unfavorite(activity.id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["activity_id"] == ^favorite_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 1
end
test "when unboosting posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, activity} =
CommonAPI.post(remote_one, %{status: "This is an unpleasant post"})
{:ok, %{id: repeat_id} = _repeat} =
CommonAPI.repeat(activity.id, local_user)
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"activity_id" => ^repeat_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The unrepeat should have triggered cancelling the publish_one jobs
assert {:ok, _unfavorite} = CommonAPI.unrepeat(activity.id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["activity_id"] == ^repeat_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
test "when unreacting to posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, %{id: activity_id}} =
CommonAPI.post(remote_one, %{status: "Gang gang!!!!"})
{:ok, %{id: react_id} = _react} =
CommonAPI.react_with_emoji(activity_id, local_user, "👍")
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"activity_id" => ^react_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The unreact should have triggered cancelling the publish_one jobs
assert {:ok, _unreact} = CommonAPI.unreact_with_emoji(activity_id, local_user, "👍")
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["activity_id"] == ^react_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
end
end
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 16281393d..6cb5934de 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -1,183 +1,358 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PollViewTest do
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.PollView
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "renders a poll" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Is Tenshi eating a corndog cute?",
poll: %{
options: ["absolutely!", "sure", "yes", "why are you even asking?"],
expires_in: 20
}
})
object = Object.normalize(activity, fetch: false)
expected = %{
emojis: [],
expired: false,
id: to_string(object.id),
multiple: false,
options: [
%{title: "absolutely!", votes_count: 0},
%{title: "sure", votes_count: 0},
%{title: "yes", votes_count: 0},
%{title: "why are you even asking?", votes_count: 0}
],
votes_count: 0,
voters_count: 0,
pleroma: %{non_anonymous: false}
}
result = PollView.render("show.json", %{object: object})
expires_at = result.expires_at
result = Map.delete(result, :expires_at)
assert result == expected
expires_at = NaiveDateTime.from_iso8601!(expires_at)
assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
end
test "detects if it is multiple choice" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which Mastodon developer is your favourite?",
poll: %{
options: ["Gargron", "Eugen"],
expires_in: 20,
multiple: true
}
})
voter = insert(:user)
object = Object.normalize(activity, fetch: false)
{:ok, _votes, object} = CommonAPI.vote(object, voter, [0, 1])
assert match?(
%{
multiple: true,
voters_count: 1,
votes_count: 2
},
PollView.render("show.json", %{object: object})
)
end
test "detects emoji" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "What's with the smug face?",
poll: %{
options: [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
expires_in: 20
}
})
object = Object.normalize(activity, fetch: false)
assert %{emojis: [%{shortcode: "blank"}]} = PollView.render("show.json", %{object: object})
end
test "detects vote status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which input devices do you use?",
poll: %{
options: ["mouse", "trackball", "trackpoint"],
multiple: true,
expires_in: 20
}
})
object = Object.normalize(activity, fetch: false)
{:ok, _, object} = CommonAPI.vote(object, other_user, [1, 2])
result = PollView.render("show.json", %{object: object, for: other_user})
assert result[:voted] == true
assert 1 in result[:own_votes]
assert 2 in result[:own_votes]
assert Enum.at(result[:options], 1)[:votes_count] == 1
assert Enum.at(result[:options], 2)[:votes_count] == 1
end
test "does not crash on polls with no end date" do
object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i", fetch: true)
result = PollView.render("show.json", %{object: object})
assert result[:expires_at] == nil
assert result[:expired] == false
end
test "doesn't strips HTML tags" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "What's with the smug face?",
poll: %{
options: [
"<input type=\"date\">",
"<input type=\"date\" >",
"<input type=\"date\"/>",
"<input type=\"date\"></input>"
],
expires_in: 20
}
})
object = Object.normalize(activity, fetch: false)
assert %{
options: [
%{title: "<input type=\"date\">", votes_count: 0},
%{title: "<input type=\"date\" >", votes_count: 0},
%{title: "<input type=\"date\"/>", votes_count: 0},
%{title: "<input type=\"date\"></input>", votes_count: 0}
]
} = PollView.render("show.json", %{object: object})
end
test "displays correct voters count" do
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 14
end
test "detects that poll is non anonymous" do
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
result = PollView.render("show.json", %{object: object})
assert result[:pleroma][:non_anonymous] == true
end
+
+ test "prefers votersCount over voters list when both are present" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+
+ object = %{
+ object
+ | data: Map.put(object.data, "votersCount", 42)
+ }
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 42
+ end
+
+ test "falls back to voters list when votersCount is absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert length(object.data["voters"]) == 1
+
+ data = Map.delete(object.data, "votersCount")
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 1
+ end
+
+ test "returns 0 when both votersCount and voters are absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.delete("voters")
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "returns 0 when voters list is empty" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.put("voters", [])
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "does not inflate votersCount when same voter picks multiple options" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+ end
+
+ test "preserves votersCount from remote source when existing voter picks another option" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
+
+ object = %{object | data: Map.put(object.data, "votersCount", 14)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 14
+ end
+
+ test "returns 0 when votersCount is explicitly 0" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ object = %{object | data: Map.put(object.data, "votersCount", 0)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "falls back to voters list when votersCount is nil" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ object = %{object | data: Map.put(object.data, "votersCount", nil)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == length(object.data["voters"])
+ end
end
diff --git a/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
new file mode 100644
index 000000000..8ace74dfb
--- /dev/null
+++ b/test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
@@ -0,0 +1,121 @@
+# 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.EnsureHostMatchesPlugTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
+
+ import Plug.Conn
+ import Tesla.Mock
+
+ setup do
+ mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
+ defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]}
+
+ describe "EnsureHostMatchesPlug" do
+ setup do
+ conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true)
+ [conn: conn]
+ end
+
+ test "gracefully handles no Host header", %{conn: conn} do
+ conn = EnsureHostMatchesPlug.call(conn, %{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header not provided"
+ end
+
+ test "gracefully handles empty Host header", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header not provided"
+ end
+
+ test "it rejects Host header not matching Endpoint URL", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("invalid.example.com")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects Host header not matching Endpoint with port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("invalid.example.com:#{endpoint.port}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects Host header not matching Endpoint port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}:25")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "Host header does not match this instance"
+ end
+
+ test "it rejects multiple Host headers", %{conn: conn} do
+ conn =
+ conn
+ |> set_host("host1.example.com")
+ |> set_host("host2.example.com")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.status == 400
+ assert conn.halted == true
+ assert conn.resp_body == "More than one Host header provided"
+ end
+
+ test "it works for Host header without port", %{conn: conn} do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.halted == false
+ assert Map.get(conn.assigns, :valid_host_header, nil)
+ end
+
+ test "it works for Host header with port same as Endpoint", %{
+ conn: conn
+ } do
+ endpoint = URI.parse(Endpoint.url())
+
+ conn =
+ conn
+ |> set_host("#{endpoint.host}:#{endpoint.port}")
+ |> EnsureHostMatchesPlug.call(%{})
+
+ assert conn.halted == false
+ assert Map.get(conn.assigns, :valid_host_header, nil)
+ end
+ end
+end
diff --git a/test/pleroma/web/plugs/remote_ip_test.exs b/test/pleroma/web/plugs/remote_ip_test.exs
index 37b751370..19e786f8a 100644
--- a/test/pleroma/web/plugs/remote_ip_test.exs
+++ b/test/pleroma/web/plugs/remote_ip_test.exs
@@ -1,109 +1,143 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RemoteIpTest do
use ExUnit.Case
import Plug.Conn
import Plug.Test
alias Pleroma.Web.Plugs.RemoteIp
import Pleroma.Tests.Helpers, only: [clear_config: 2]
setup do:
clear_config(RemoteIp,
enabled: true,
headers: ["x-forwarded-for"],
proxies: [],
reserved: [
"127.0.0.0/8",
"::1/128",
"fc00::/7",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
]
)
test "disabled" do
clear_config(RemoteIp, enabled: false)
%{remote_ip: remote_ip} = conn(:get, "/")
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == remote_ip
end
test "enabled" do
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom headers" do
clear_config(RemoteIp, enabled: true, headers: ["cf-connecting-ip"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
conn =
conn(:get, "/")
|> put_req_header("cf-connecting-ip", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom proxies" do
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
clear_config([RemoteIp, :proxies], ["173.245.48.0/20"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "proxies set without CIDR format" do
clear_config([RemoteIp, :proxies], ["173.245.48.1"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "proxies set `nonsensical` CIDR" do
clear_config([RemoteIp, :reserved], ["127.0.0.0/8"])
clear_config([RemoteIp, :proxies], ["10.0.0.3/24"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "10.0.0.3, 1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
+
+ test "reserved ranges are configurable" do
+ clear_config([RemoteIp, :reserved], [])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
+
+ test "clients override reserved ranges" do
+ clear_config([RemoteIp, :clients], ["10.0.0.0/8"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
+
+ test "clients override proxies" do
+ clear_config([RemoteIp, :clients], ["10.0.0.3"])
+ clear_config([RemoteIp, :proxies], ["10.0.0.0/8"])
+
+ conn =
+ conn(:get, "/")
+ |> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
+ |> RemoteIp.call(nil)
+
+ assert conn.remote_ip == {10, 0, 0, 3}
+ end
end
diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs
index 2fae83305..68ded6906 100644
--- a/test/pleroma/web/static_fe/static_fe_controller_test.exs
+++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs
@@ -1,232 +1,241 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
setup_all do: clear_config([:static_fe, :enabled], true)
setup %{conn: conn} do
conn = put_req_header(conn, "accept", "text/html")
user = insert(:user)
%{conn: conn, user: user}
end
describe "user profile html" do
test "just the profile as HTML", %{conn: conn, user: user} do
conn = get(conn, "/users/#{user.nickname}")
assert html_response(conn, 200) =~ user.nickname
end
+ test "renders profile HTML inside the default app layout", %{conn: conn, user: user} do
+ conn = get(conn, "/users/#{user.nickname}")
+
+ html = html_response(conn, 200)
+ assert html =~ "<!DOCTYPE html>"
+ assert html =~ ~s(class="instance-header")
+ assert html =~ ~s(<link rel="stylesheet" href="/instance/static.css">)
+ end
+
test "404 when user not found", %{conn: conn} do
conn = get(conn, "/users/limpopo")
assert html_response(conn, 404) =~ "not found"
end
test "profile does not include private messages", %{conn: conn, user: user} do
CommonAPI.post(user, %{status: "public"})
CommonAPI.post(user, %{status: "private", visibility: "private"})
conn = get(conn, "/users/#{user.nickname}")
html = html_response(conn, 200)
assert html =~ ">public<"
refute html =~ ">private<"
end
test "pagination", %{conn: conn, user: user} do
Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end)
conn = get(conn, "/users/#{user.nickname}")
html = html_response(conn, 200)
assert html =~ ">test30<"
assert html =~ ">test11<"
refute html =~ ">test10<"
refute html =~ ">test1<"
end
test "pagination, page 2", %{conn: conn, user: user} do
activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{status: "test#{i}"}) end)
{:ok, a11} = Enum.at(activities, 11)
conn = get(conn, "/users/#{user.nickname}?max_id=#{a11.id}")
html = html_response(conn, 200)
assert html =~ ">test1<"
assert html =~ ">test10<"
refute html =~ ">test20<"
refute html =~ ">test29<"
end
test "does not require authentication on non-federating instances", %{
conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
conn = get(conn, "/users/#{user.nickname}")
assert html_response(conn, 200) =~ user.nickname
end
test "returns 404 for local user with `restrict_unauthenticated/profiles/local` setting", %{
conn: conn
} do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
local_user = insert(:user, local: true)
conn
|> get("/users/#{local_user.nickname}")
|> html_response(404)
end
end
describe "notice html" do
test "single notice page", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn = get(conn, "/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ "<header>"
assert html =~ user.nickname
assert html =~ "testing a thing!"
end
test "redirects to json if requested", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn =
conn
|> put_req_header(
"accept",
"Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html"
)
|> get("/notice/#{activity.id}")
assert redirected_to(conn, 302) =~ activity.data["object"]
end
test "filters HTML tags", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "<script>alert('xss')</script>"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ ~s[&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;]
end
test "shows the whole thread", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "space: the final frontier"})
CommonAPI.post(user, %{
status: "these are the voyages or something",
in_reply_to_status_id: activity.id
})
conn = get(conn, "/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ "the final frontier"
assert html =~ "voyages"
end
test "redirect by AP object ID", %{conn: conn, user: user} do
{:ok, %Activity{data: %{"object" => object_url}}} =
CommonAPI.post(user, %{status: "beam me up"})
conn = get(conn, URI.parse(object_url).path)
assert html_response(conn, 302) =~ "redirected"
end
test "redirect by activity ID", %{conn: conn, user: user} do
{:ok, %Activity{data: %{"id" => id}}} =
CommonAPI.post(user, %{status: "I'm a doctor, not a devops!"})
conn = get(conn, URI.parse(id).path)
assert html_response(conn, 302) =~ "redirected"
end
test "404 when notice not found", %{conn: conn} do
conn = get(conn, "/notice/88c9c317")
assert html_response(conn, 404) =~ "not found"
end
test "404 for private status", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "don't show me!", visibility: "private"})
conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 404) =~ "not found"
end
test "302 for remote cached status", %{conn: conn, user: user} do
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => user.ap_id,
"object" => %{
"to" => user.follower_address,
"cc" => "https://www.w3.org/ns/activitystreams#Public",
"id" => Utils.generate_object_id(),
"content" => "blah blah blah",
"type" => "Note",
"attributedTo" => user.ap_id
}
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 302) =~ "redirected"
end
test "does not require authentication on non-federating instances", %{
conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 200) =~ "testing a thing!"
end
test "returns 404 for local public activity with `restrict_unauthenticated/activities/local` setting",
%{conn: conn, user: user} do
clear_config([:restrict_unauthenticated, :activities, :local], true)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn
|> get("/notice/#{activity.id}")
|> html_response(404)
end
end
end
diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs
index 94dd5f6c1..3806ecac9 100644
--- a/test/pleroma/workers/signature_retry_worker_test.exs
+++ b/test/pleroma/workers/signature_retry_worker_test.exs
@@ -1,574 +1,635 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SignatureRetryWorkerTest do
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import ExUnit.CaptureLog
import Pleroma.Factory
@moduletag capture_log: true
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
defp signature_headers_for(%User{} = signer) do
[
- {"host", "local.test"},
+ {"host", "#{URI.parse(Endpoint.url()).host}"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
{
"signature",
"keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
}
]
end
defp stub_actor_fetch(%User{} = signer) do
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
Tesla.Mock.mock(fn
%{url: url} when url == signer.ap_id ->
%Tesla.Env{
status: 200,
body: Jason.encode!(signer_json),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
end
defp expect_signature_from(%User{} = signer) do
stub_actor_fetch(signer)
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
end
defp enqueue_failed_signature(params, signer) do
Federator.incoming_failed_signature_ap_doc(%{
method: "POST",
req_headers: signature_headers_for(signer),
request_path: "/inbox",
params: params,
query_string: ""
})
end
defp failed_signature_job(params, req_headers, opts \\ []) do
%Oban.Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => Keyword.get(opts, :method, "POST"),
"req_headers" => req_headers,
"request_path" => Keyword.get(opts, :request_path, "/inbox"),
"params" => params,
"query_string" => Keyword.get(opts, :query_string, "")
}
}
end
defp assert_mismatched_signature_cancelled(params, signer) do
assert {:ok, oban_job} = enqueue_failed_signature(params, signer)
capture_log([level: :warning], fn ->
assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job)
end)
end
test "Federator preserves request metadata for failed-signature retry jobs" do
params = insert(:note_activity).data
req_headers = [
{"host", "local.test"},
{"signature", "keyId=\"https://one.com/users/alice#main-key\""}
]
assert {:ok, oban_job} =
Federator.incoming_failed_signature_ap_doc(%{
method: "POST",
req_headers: req_headers,
request_path: "/inbox",
params: params,
query_string: "foo=bar"
})
assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker"
assert %{
"op" => "incoming_failed_signature_ap_doc",
"method" => "POST",
"req_headers" => ^req_headers,
"request_path" => "/inbox",
"params" => ^params,
"query_string" => "foo=bar"
} = oban_job.args
end
test "cancels retry jobs without request metadata" do
params = insert(:note_activity).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :missing_signature_retry_metadata} =
SignatureRetryWorker.perform(%Oban.Job{
args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params}
})
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:missing_signature_retry_metadata"
assert log =~ "payload_actor=#{inspect(params["actor"])}"
assert log =~ "activity_id=#{inspect(params["id"])}"
assert log =~ "type=#{inspect(params["type"])}"
assert log =~ "request_path=nil"
end
test "cancels retry jobs with malformed serialized request headers" do
params = insert(:note_activity).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature_retry_metadata} =
SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]]))
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature_retry_metadata"
assert log =~ "signature_actor=nil"
assert log =~ "request_path=\"/inbox\""
end
test "cancels retry jobs without a signature header" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(
failed_signature_job(params, [{"host", "local.test"}])
)
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature"
assert log =~ "payload_actor=#{inspect(params["actor"])}"
assert log =~ "signature_actor=nil"
assert log =~ "request_path=\"/inbox\""
end
test "cancels missing signature before fetching an unavailable payload actor" do
params =
insert(:note_activity).data
|> Map.put("actor", "https://unavailable.example/users/bob")
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}]))
end
test "cancels signer mismatch before fetching an unavailable payload actor" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params =
insert(:note_activity).data
|> Map.put("actor", "https://unavailable.example/users/bob")
assert {:cancel, :actor_signature_mismatch} =
SignatureRetryWorker.perform(
failed_signature_job(params, signature_headers_for(alice))
)
end
test "cancels retry jobs with a signature header without keyId" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}]
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
end
test "cancels retry jobs with an unparsable signature keyId" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}]
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
end
test "cancels when the refetched key still cannot validate the signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/invalid-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/invalid-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
stub_actor_fetch(alice)
assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job)
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature"
assert log =~ "payload_actor=\"https://one.com/users/alice\""
assert log =~ "signature_actor=\"https://one.com/users/alice\""
assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
assert log =~ "type=\"Create\""
assert log =~ "request_path=\"/inbox\""
refute Activity.get_by_ap_id(create["id"])
end
+ test "cancels when the Host header does not match Endpoint" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/invalid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/invalid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_from(alice)
+
+ headers =
+ [
+ {"host", "invalid.example.com"},
+ {"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
+ {"digest", "SHA-256=fake-digest"},
+ {"content-type", "application/activity+json"},
+ {
+ "signature",
+ "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ }
+ ]
+
+ assert {:ok, oban_job} =
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: headers,
+ request_path: "/inbox",
+ params: create,
+ query_string: ""
+ })
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job)
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:host_header_mismatch"
+ assert log =~ "payload_actor=\"https://one.com/users/alice\""
+ assert log =~ "signature_actor=\"https://one.com/users/alice\""
+ assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
+ assert log =~ "type=\"Create\""
+ assert log =~ "request_path=\"/inbox\""
+
+ refute Activity.get_by_ap_id(create["id"])
+ end
+
test "processes the activity after refetching a valid matching signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/valid-signature-create",
"context" => "https://one.com/contexts/valid-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/valid-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"context" => "https://one.com/contexts/valid-signature-create",
"content" => "valid post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_from(alice)
assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job)
assert Activity.get_by_ap_id(create["id"])
end
test "processes the activity when a real signature validates with a query string" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/valid-query-signature-create",
"context" => "https://one.com/contexts/valid-query-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/valid-query-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"context" => "https://one.com/contexts/valid-query-signature-create",
"content" => "valid signed post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
stub_actor_fetch(alice)
date = "Thu, 25 Jul 2024 13:33:31 GMT"
digest = "SHA-256=fake-digest"
signature =
Signature.sign(alice, %{
"(request-target)" => "post /inbox?foo=bar",
"content-type" => "application/activity+json",
date: date,
digest: digest,
- host: "local.test"
+ host: "#{URI.parse(Endpoint.url()).host}"
})
req_headers = [
- ["host", "local.test"],
+ ["host", "#{URI.parse(Endpoint.url()).host}"],
["date", date],
["digest", digest],
["content-type", "application/activity+json"],
["signature", signature]
]
assert {:ok, %Activity{}} =
SignatureRetryWorker.perform(
failed_signature_job(create, req_headers, query_string: "foo=bar")
)
assert Activity.get_by_ap_id(create["id"])
end
test "cancels when signature actor does not match payload actor" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note =
insert(:note,
user: bob,
object_local: false,
data: %{"id" => "https://two.com/objects/malicious-update-note"}
)
update = %{
"type" => "Update",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/malicious-update",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data
}
assert_mismatched_signature_cancelled(update, alice)
end
test "cancels signature actor mismatch through Federator-created jobs" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note =
insert(:note,
user: bob,
object_local: false,
data: %{"id" => "https://two.com/objects/federator-malicious-note"}
)
update = %{
"type" => "Update",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/federator-malicious-update",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data
}
assert_mismatched_signature_cancelled(update, alice)
end
test "cancels signature actor mismatch before processing a forged Create" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
end
test "cancels signature actor mismatch when payload actor is embedded" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => %{"id" => bob.ap_id},
"id" => "https://two.com/activities/embedded-actor-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/embedded-actor-forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
end
test "logs signature actor mismatch retry rejections" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/logged-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/logged-forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
log = assert_mismatched_signature_cancelled(create, alice)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:actor_signature_mismatch"
assert log =~ "payload_actor=\"https://two.com/users/bob\""
assert log =~ "signature_actor=\"https://one.com/users/alice\""
assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\""
assert log =~ "type=\"Create\""
assert log =~ "request_path=\"/inbox\""
end
test "cancels signature actor mismatch before actually creating a forged post" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/actually-forged-note"
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/actually-forged-create",
"context" => "https://two.com/contexts/actually-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/actually-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
refute Object.get_by_ap_id(object_id)
end
test "cancels signature actor mismatch before processing a forged Like" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
like = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(like, alice)
end
test "cancels signature actor mismatch before actually creating a forged Like" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
like = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/actually-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(like, alice)
refute Activity.get_by_ap_id(like["id"])
end
test "cancels signature actor mismatch before processing a forged Announce" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
announce = %{
"type" => "Announce",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-announce",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(announce, alice)
end
test "cancels signature actor mismatch before processing a forged Follow" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
followed = insert(:user)
follow = %{
"type" => "Follow",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-follow",
"to" => [followed.ap_id],
"cc" => [],
"object" => followed.ap_id
}
assert_mismatched_signature_cancelled(follow, alice)
end
test "cancels signature actor mismatch before processing a forged Undo" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
undo = %{
"type" => "Undo",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-undo",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => "https://two.com/activities/existing-bob-activity"
}
assert_mismatched_signature_cancelled(undo, alice)
end
end
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index f010fec33..c01516169 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -1,125 +1,128 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build common datastructures and query the data layer.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
alias Pleroma.DataCase
using do
quote do
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
use Pleroma.Tests.Helpers
import Pleroma.Web.Router.Helpers
alias Pleroma.Config
# The default endpoint for testing
@endpoint Pleroma.Web.Endpoint
# Sets up OAuth access with specified scopes
defp oauth_access(scopes, opts \\ []) do
user =
Keyword.get_lazy(opts, :user, fn ->
Pleroma.Factory.insert(:user)
end)
token =
Keyword.get_lazy(opts, :oauth_token, fn ->
Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes)
end)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, token)
%{user: user, token: token, conn: conn}
end
defp request_content_type(%{conn: conn}) do
conn = put_req_header(conn, "content-type", "multipart/form-data")
[conn: conn]
end
defp empty_json_response(conn) do
body = response(conn, 204)
response_content_type(conn, :json)
body
end
defp json_response_and_validate_schema(
%{private: %{operation_id: op_id}} = conn,
status
) do
{spec, lookup} = OpenApiSpex.Plug.PutApiSpec.get_spec_and_operation_lookup(conn)
content_type =
conn
|> Plug.Conn.get_resp_header("content-type")
|> List.first()
|> String.split(";")
|> List.first()
status = Plug.Conn.Status.code(status)
unless lookup[op_id].responses[status] do
err = "Response schema not found for #{status} #{conn.method} #{conn.request_path}"
flunk(err)
end
schema = lookup[op_id].responses[status].content[content_type].schema
json = if status == 204, do: empty_json_response(conn), else: json_response(conn, status)
case OpenApiSpex.cast_value(json, schema, spec) do
{:ok, _data} ->
json
{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = OpenApiSpex.Cast.Error.message(error)
path = OpenApiSpex.Cast.Error.path_to_string(error)
"#{message} at #{path}"
end)
flunk(
"Response does not conform to schema of #{op_id} operation: #{Enum.join(errors, "\n")}\n#{inspect(json)}"
)
end
end
defp json_response_and_validate_schema(conn, _status) do
flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}")
end
end
end
setup tags do
DataCase.setup_multi_process_mode(tags)
DataCase.setup_streamer(tags)
DataCase.stub_pipeline()
Mox.verify_on_exit!()
+ endpoint = URI.parse(Pleroma.Web.Endpoint.url())
+ conn = Phoenix.ConnTest.build_conn()
+ conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]}
- {:ok, conn: Phoenix.ConnTest.build_conn()}
+ {:ok, conn: conn}
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 9:50 PM (16 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800584
Default Alt Text
(1 MB)

Event Timeline