diff --git a/.gitignore b/.gitignore index 198e80139..599b52b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /*.ez /test/uploads /.elixir_ls +/test/fixtures/DSCN0010_tmp.jpg /test/fixtures/test_tmp.txt /test/fixtures/image_tmp.jpg /test/tmp/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b4bd59b43..c9ab84892 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -58,26 +58,25 @@ unit-testing: alias: postgres command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] script: + - apt-get update && apt-get install -y libimage-exiftool-perl - mix deps.get - mix ecto.create - mix ecto.migrate - mix coveralls --preload-modules -# Removed to fix CI issue. In this early state it wasn't adding much value anyway. -# TODO Fix and reinstate federated testing -# federated-testing: -# stage: test -# cache: *testing_cache_policy -# services: -# - name: minibikini/postgres-with-rum:12 -# alias: postgres -# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] -# script: -# - mix deps.get -# - mix ecto.create -# - mix ecto.migrate -# - epmd -daemon -# - mix test --trace --only federated +federated-testing: + stage: test + cache: *testing_cache_policy + services: + - name: minibikini/postgres-with-rum:12 + alias: postgres + command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] + script: + - mix deps.get + - mix ecto.create + - mix ecto.migrate + - epmd -daemon + - mix test --trace --only federated unit-testing-rum: stage: test @@ -91,6 +90,7 @@ unit-testing-rum: <<: *global_variables RUM_ENABLED: "true" script: + - apt-get update && apt-get install -y libimage-exiftool-perl - mix deps.get - mix ecto.create - mix ecto.migrate diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 66fbc510e..9ce9b6918 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -14,7 +14,7 @@ * Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE): * Elixir version (`elixir -v` for from source installations, N/A for OTP): * Operating system: -* PostgreSQL version (`postgres -V`): +* PostgreSQL version (`psql -V`): ### Bug description diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3bda99e..b124b59e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - **Breaking:** Elixir >=1.9 is now required (was >= 1.8) +- **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated. - In Conversations, return only direct messages as `last_status` - Using the `only_media` filter on timelines will now exclude reblog media - MFR policy to set global expiration for all local Create activities - OGP rich media parser merged with TwitterCard - Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated. -- **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated. +- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
API Changes +- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed. +- **Breaking:** Image description length is limited now. - **Breaking:** Emoji API: changed methods and renamed routes. +- MastodonAPI: Allow removal of avatar, banner and background. +- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream. +- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance +- Mastodon API: On deletion, returns the original post text. +- Mastodon API: Add `pleroma.unread_count` to the Marker entity. +- **Breaking:** Notification Settings API for suppressing notifications + has been simplified down to `block_from_strangers`. +- **Breaking:** Notification Settings API option for hiding push notification + contents has been renamed to `hide_notification_contents`
@@ -33,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added +- Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation. - Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - Instance: Add `background_image` to configuration and `/api/v1/instance` @@ -49,14 +62,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `:reject_deletes` group to SimplePolicy - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - Support pagination in emoji packs API (for packs and for files in pack) +- Support for viewing instances favicons next to posts and accounts +- Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
API Changes +- Mastodon API: Add pleroma.parents_visible field to statuses. - Mastodon API: Extended `/api/v1/instance`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. -- Mastodon API: Add support for filtering replies in public and home timelines -- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials` +- Mastodon API: Add support for filtering replies in public and home timelines. +- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`. +- Mastodon API: Support irreversible property for filters. +- Mastodon API: Add pleroma.favicon field to accounts. - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. - OTP: Add command to reload emoji packs @@ -70,6 +88,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Resolving Peertube accounts with Webfinger - `blob:` urls not being allowed by connect-src CSP - Mastodon API: fix `GET /api/v1/notifications` not returning the full result set +- Rich Media Previews for Twitter links +- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated` +- Fix CSP policy generation to include remote Captcha services +- Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance. ## [Unreleased (patch)] @@ -211,7 +233,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: `pleroma.thread_muted` to the Status entity - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. -- Mastodon API: Add `pleroma.unread_count` to the Marker entity - Admin API: Render whole status in grouped reports - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. diff --git a/Dockerfile b/Dockerfile index 29931a5e3..0f4fcd0bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ - apk add imagemagick ncurses postgresql-client &&\ + apk add exiftool imagemagick ncurses postgresql-client &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\ mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/static &&\ diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index 074ded457..f5c7bfce8 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do @visibility ~w(public private direct unlisted) @types [ :simple, + :simple_filtered, :emoji, :mentions, :hell_thread, @@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do insert_local_activity(visibility, group, users, "Simple status") end + defp insert_activity(:simple_filtered, visibility, group, users, _opts) + when group in @remote_groups do + insert_remote_activity(visibility, group, users, "Remote status which must be filtered") + end + + defp insert_activity(:simple_filtered, visibility, group, users, _opts) do + insert_local_activity(visibility, group, users, "Simple status which must be filtered") + end + defp insert_activity(:emoji, visibility, group, users, _opts) when group in @remote_groups do insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 15fd06c3d..dfbd916be 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -32,10 +32,22 @@ defp fetch_user(user) do ) end + defp create_filter(user) do + Pleroma.Filter.create(%Pleroma.Filter{ + user_id: user.id, + phrase: "must be filtered", + hide: true + }) + end + + defp delete_filter(filter), do: Repo.delete(filter) + defp fetch_timelines(user) do fetch_home_timeline(user) + fetch_home_timeline_with_filter(user) fetch_direct_timeline(user) fetch_public_timeline(user) + fetch_public_timeline_with_filter(user) fetch_public_timeline(user, :with_blocks) fetch_public_timeline(user, :local) fetch_public_timeline(user, :tag) @@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do } end - defp fetch_home_timeline(user) do + defp fetch_home_timeline(user, title_end \\ "") do opts = opts_for_home_timeline(user) recipients = [user.ap_id | User.following(user)] @@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do |> Enum.reverse() |> List.last() + title = "home timeline " <> title_end + Benchee.run( %{ - "home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end + title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end }, inputs: %{ "1 page" => opts, @@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do ) end + defp fetch_home_timeline_with_filter(user) do + {:ok, filter} = create_filter(user) + + fetch_home_timeline(user, "with filters") + + delete_filter(filter) + end + defp opts_for_direct_timeline(user) do %{ visibility: "direct", @@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do fetch_public_timeline(opts, "public timeline") end + defp fetch_public_timeline_with_filter(user) do + {:ok, filter} = create_filter(user) + opts = opts_for_public_timeline(user) + + fetch_public_timeline(opts, "public timeline with filters") + delete_filter(filter) + end + defp fetch_public_timeline(user, :local) do opts = opts_for_public_timeline(user, :local) diff --git a/config/config.exs b/config/config.exs index a74a6a5ba..406bf2a9b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -97,6 +97,7 @@ "dat", "dweb", "gopher", + "hyper", "ipfs", "ipns", "irc", @@ -171,7 +172,7 @@ "application/ld+json" => ["activity+json"] } -config :tesla, adapter: Tesla.Adapter.Hackney +config :tesla, adapter: Tesla.Adapter.Gun # Configures http settings, upstream proxy etc. config :pleroma, :http, @@ -188,6 +189,7 @@ background_image: "/images/city.jpg", instance_thumbnail: "/instance/thumbnail.jpeg", limit: 5_000, + description_limit: 5_000, chat_limit: 5_000, remote_limit: 100_000, upload_limit: 16_000_000, @@ -434,6 +436,11 @@ ], unfurl_nsfw: false +config :pleroma, Pleroma.Web.Preload, + providers: [ + Pleroma.Web.Preload.Providers.Instance + ] + config :pleroma, :http_security, enabled: true, sts: false, @@ -491,8 +498,7 @@ config :pleroma, Oban, repo: Pleroma.Repo, - verbose: false, - prune: {:maxlen, 1500}, + log: false, queues: [ activity_expiration: 10, federator_incoming: 50, @@ -506,6 +512,7 @@ attachments_cleanup: 5, new_users_digest: 1 ], + plugins: [Oban.Plugins.Pruner], crontab: [ {"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker}, {"0 * * * *", Pleroma.Workers.Cron.StatsWorker}, @@ -639,32 +646,30 @@ prepare: :unnamed config :pleroma, :connections_pool, - checkin_timeout: 250, + reclaim_multiplier: 0.1, + connection_acquisition_wait: 250, + connection_acquisition_retries: 5, max_connections: 250, - retry: 1, - retry_timeout: 1000, + max_idle_time: 30_000, + retry: 0, await_up_timeout: 5_000 config :pleroma, :pools, federation: [ size: 50, - max_overflow: 10, - timeout: 150_000 + max_waiting: 10 ], media: [ size: 50, - max_overflow: 10, - timeout: 150_000 + max_waiting: 10 ], upload: [ size: 25, - max_overflow: 5, - timeout: 300_000 + max_waiting: 5 ], default: [ size: 10, - max_overflow: 2, - timeout: 10_000 + max_waiting: 2 ] config :pleroma, :hackney_pools, @@ -697,6 +702,8 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws +config :pleroma, :instances_favicons, enabled: false + # 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 204de8324..a936b7abf 100644 --- a/config/description.exs +++ b/config/description.exs @@ -23,29 +23,26 @@ key: :uploader, type: :module, description: "Module which will be used for uploads", - suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.S3] + 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: - Generator.list_modules_in_dir( - "lib/pleroma/upload/filter", - "Elixir.Pleroma.Upload.Filter." - ) + 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`." + "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, needed if you use CDN", + description: "Base URL for the uploads, needed if you use CDN", suggestions: [ "https://cdn-host.com" ] @@ -58,6 +55,7 @@ }, %{ key: :proxy_opts, + label: "Proxy Options", type: :keyword, description: "Options for Pleroma.ReverseProxy", suggestions: [ @@ -85,6 +83,7 @@ }, %{ key: :http, + label: "HTTP", type: :keyword, description: "HTTP options", children: [ @@ -193,7 +192,9 @@ %{ key: :args, type: [:string, {:list, :string}, {:list, :tuple}], - description: "List of actions for the mogrify command", + 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>\"}]` string will be parsed into list of the settings.", suggestions: [ "strip", "auto-orient", @@ -479,6 +480,7 @@ %{ group: :pleroma, key: :uri_schemes, + label: "URI Schemes", type: :group, description: "URI schemes related settings", children: [ @@ -492,6 +494,7 @@ "dat", "dweb", "gopher", + "hyper", "ipfs", "ipns", "irc", @@ -651,17 +654,17 @@ key: :invites_enabled, type: :boolean, description: - "Enable user invitations for admins (depends on `registrations_open` being disabled)." + "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." + description: "Require users to confirm their emails before signing in" }, %{ key: :federating, type: :boolean, - description: "Enable federation with other instances." + description: "Enable federation with other instances" }, %{ key: :federation_incoming_replies_max_depth, @@ -679,7 +682,7 @@ label: "Fed. reachability timeout days", type: :integer, description: - "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.", + "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it", suggestions: [ 7 ] @@ -693,8 +696,9 @@ key: :public, type: :boolean, description: - "Makes the client API in authentificated mode-only except for user-profiles." <> - " Useful for disabling the Local Timeline and The Whole Known Network." + "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, @@ -801,6 +805,7 @@ }, %{ 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." <> @@ -840,7 +845,7 @@ %{ key: :skip_thread_containment, type: :boolean, - description: "Skip filtering out broken threads. Default: enabled" + description: "Skip filtering out broken threads. Default: enabled." }, %{ key: :limit_to_local_content, @@ -904,6 +909,7 @@ children: [ %{ key: :totp, + label: "TOTP settings", type: :keyword, description: "TOTP settings", suggestions: [digits: 6, period: 30], @@ -920,7 +926,7 @@ type: :integer, suggestions: [30], description: - "a period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds." + "A period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds." } ] }, @@ -934,7 +940,7 @@ key: :number, type: :integer, suggestions: [5], - description: "number of backup codes to generate." + description: "Number of backup codes to generate." }, %{ key: :length, @@ -974,6 +980,7 @@ group: :logger, type: :group, key: :ex_syslogger, + label: "ExSyslogger", description: "ExSyslogger-related settings", children: [ %{ @@ -992,7 +999,7 @@ %{ key: :format, type: :string, - description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".", + description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"", suggestions: ["$metadata[$level] $message"] }, %{ @@ -1006,6 +1013,7 @@ group: :logger, type: :group, key: :console, + label: "Console Logger", description: "Console logger settings", children: [ %{ @@ -1017,7 +1025,7 @@ %{ key: :format, type: :string, - description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".", + description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"", suggestions: ["$metadata[$level] $message"] }, %{ @@ -1030,6 +1038,7 @@ %{ group: :quack, type: :group, + label: "Quack Logger", description: "Quack-related settings", children: [ %{ @@ -1058,6 +1067,7 @@ }, %{ key: :webhook_url, + label: "Webhook URL", type: :string, description: "Configure the Slack incoming webhook", suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"] @@ -1140,19 +1150,19 @@ key: :greentext, label: "Greentext", type: :boolean, - description: "Enables green text on lines prefixed with the > character." + description: "Enables green text on lines prefixed with the > character" }, %{ key: :hideFilteredStatuses, label: "Hide Filtered Statuses", type: :boolean, - description: "Hides filtered statuses from timelines." + description: "Hides filtered statuses from timelines" }, %{ key: :hideMutedPosts, label: "Hide Muted Posts", type: :boolean, - description: "Hides muted statuses from timelines." + description: "Hides muted statuses from timelines" }, %{ key: :hidePostStats, @@ -1164,7 +1174,7 @@ key: :hideSitename, label: "Hide Sitename", type: :boolean, - description: "Hides instance name from PleromaFE banner." + description: "Hides instance name from PleromaFE banner" }, %{ key: :hideUserStats, @@ -1209,14 +1219,14 @@ label: "NSFW Censor Image", type: :string, description: - "URL of the image to use for hiding NSFW media attachments in the timeline.", + "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.", + description: "Default post formatting option", suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] }, %{ @@ -1245,14 +1255,14 @@ key: :sidebarRight, label: "Sidebar on Right", type: :boolean, - description: "Change alignment of sidebar and panels to the right." + 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." + "Enables panel displaying functionality of the instance on the About page" }, %{ key: :showInstanceSpecificPanel, @@ -1310,7 +1320,7 @@ key: :mascots, type: {:keyword, :map}, description: - "Keyword of mascots, each element must contain both an url and a mime_type key", + "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", @@ -1334,7 +1344,7 @@ %{ key: :default_user_avatar, type: :string, - description: "URL of the default user avatar.", + description: "URL of the default user avatar", suggestions: ["/images/avi.png"] } ] @@ -1344,7 +1354,7 @@ key: :manifest, type: :group, description: - "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE", + "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.", children: [ %{ key: :icons, @@ -1380,10 +1390,45 @@ }, %{ group: :pleroma, - key: :mrf_simple, - label: "MRF simple", + key: :mrf, + tab: :mrf, + label: "MRF", type: :group, - description: "Message Rewrite Facility", + description: "General MRF settings", + children: [ + %{ + key: :policies, + type: [:module, {:list, :module}], + description: + "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.", + suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF} + }, + %{ + key: :transparency, + label: "MRF transparency", + type: :boolean, + description: + "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" + }, + %{ + key: :transparency_exclusions, + label: "MRF transparency exclusions", + type: {:list, :string}, + description: + "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", + suggestions: [ + "exclusion.com" + ] + } + ] + }, + %{ + group: :pleroma, + key: :mrf_simple, + tab: :mrf, + label: "MRF Simple", + type: :group, + description: "Simple ingress policies", children: [ %{ key: :media_removal, @@ -1402,7 +1447,7 @@ key: :federated_timeline_removal, type: {:list, :string}, description: - "List of instances to remove from Federated (aka The Whole Known Network) Timeline", + "List of instances to remove from the Federated (aka The Whole Known Network) Timeline", suggestions: ["example.com", "*.example.com"] }, %{ @@ -1446,14 +1491,15 @@ %{ group: :pleroma, key: :mrf_activity_expiration, + tab: :mrf, label: "MRF Activity Expiration Policy", type: :group, - description: "Adds expiration to all local Create Note activities", + description: "Adds automatic expiration to all local activities", children: [ %{ key: :days, type: :integer, - description: "Default global expiration time for all local Create activities (in days)", + description: "Default global expiration time for all local activities (in days)", suggestions: [90, 365] } ] @@ -1461,7 +1507,8 @@ %{ group: :pleroma, key: :mrf_subchain, - label: "MRF subchain", + tab: :mrf, + label: "MRF Subchain", type: :group, description: "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <> @@ -1469,7 +1516,7 @@ children: [ %{ key: :match_actor, - type: :map, + type: {:map, {:list, :string}}, description: "Matches a series of regular expressions against the actor field", suggestions: [ %{ @@ -1482,9 +1529,9 @@ %{ group: :pleroma, key: :mrf_rejectnonpublic, - description: - "MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.", - label: "MRF reject non public", + tab: :mrf, + description: "RejectNonPublic drops posts with non-public visibility settings.", + label: "MRF Reject Non Public", type: :group, children: [ %{ @@ -1503,16 +1550,17 @@ %{ group: :pleroma, key: :mrf_hellthread, - label: "MRF hellthread", + tab: :mrf, + label: "MRF Hellthread", type: :group, - description: "Block messages with too much mentions", + description: "Block messages with excessive user mentions", children: [ %{ key: :delist_threshold, type: :integer, description: - "Number of mentioned users after which the message gets delisted (the message can still be seen, " <> - " but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.", + "Number of mentioned users after which the message gets removed from timelines and" <> + "disables notifications. Set to 0 to disable.", suggestions: [10] }, %{ @@ -1527,27 +1575,28 @@ %{ group: :pleroma, key: :mrf_keyword, - label: "MRF keyword", + tab: :mrf, + label: "MRF Keyword", type: :group, description: "Reject or Word-Replace messages with a keyword or regex", children: [ %{ key: :reject, - type: [:string, :regex], + type: {:list, :string}, description: "A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.", suggestions: ["foo", ~r/foo/iu] }, %{ key: :federated_timeline_removal, - type: [:string, :regex], + type: {:list, :string}, description: "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted). Each pattern can be a string or a regular expression.", suggestions: ["foo", ~r/foo/iu] }, %{ key: :replace, - type: [{:tuple, :string, :string}, {:tuple, :regex, :string}], + type: {:list, :tuple}, description: "A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.", suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}] @@ -1557,14 +1606,15 @@ %{ group: :pleroma, key: :mrf_mention, - label: "MRF mention", + tab: :mrf, + label: "MRF Mention", type: :group, - description: "Block messages which mention a user", + description: "Block messages which mention a specific user", children: [ %{ key: :actors, type: {:list, :string}, - description: "A list of actors for which any post mentioning them will be dropped.", + description: "A list of actors for which any post mentioning them will be dropped", suggestions: ["actor1", "actor2"] } ] @@ -1572,7 +1622,8 @@ %{ group: :pleroma, key: :mrf_vocabulary, - label: "MRF vocabulary", + tab: :mrf, + label: "MRF Vocabulary", type: :group, description: "Filter messages which belong to certain activity vocabularies", children: [ @@ -1580,14 +1631,14 @@ key: :accept, type: {:list, :string}, description: - "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted", + "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.", suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] }, %{ key: :reject, type: {:list, :string}, description: - "A list of ActivityStreams terms to reject. If empty, no messages are rejected", + "A list of ActivityStreams terms to reject. If empty, no messages are rejected.", suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] } ] @@ -1617,6 +1668,7 @@ }, %{ 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.", @@ -1649,6 +1701,7 @@ }, %{ key: :proxy_opts, + label: "Proxy Options", type: :keyword, description: "Options for Pleroma.ReverseProxy", suggestions: [ @@ -1676,6 +1729,7 @@ }, %{ key: :http, + label: "HTTP", type: :keyword, description: "HTTP options", children: [ @@ -1714,8 +1768,8 @@ %{ key: :whitelist, type: {:list, :string}, - description: "List of domains to bypass the mediaproxy", - suggestions: ["example.com"] + description: "List of hosts with scheme to bypass the mediaproxy", + suggestions: ["http://example.com"] } ] }, @@ -1732,15 +1786,20 @@ }, %{ key: :headers, - type: {:list, :tuple}, - description: "HTTP headers of request.", + type: {:keyword, :string}, + description: "HTTP headers of request", suggestions: [{"x-refresh", 1}] }, %{ key: :options, type: :keyword, - description: "Request options.", - suggestions: [params: %{ts: "xxx"}] + description: "Request options", + children: [ + %{ + key: :params, + type: {:map, :string} + } + ] } ] }, @@ -1771,6 +1830,7 @@ }, %{ key: :ip, + label: "IP", type: :tuple, description: "IP address to bind to", suggestions: [{0, 0, 0, 0}] @@ -1784,7 +1844,7 @@ %{ key: :dstport, type: :integer, - description: "Port advertised in urls (optional, defaults to port)", + description: "Port advertised in URLs (optional, defaults to port)", suggestions: [9999] } ] @@ -1792,6 +1852,7 @@ %{ group: :pleroma, key: :activitypub, + label: "ActivityPub", type: :group, description: "ActivityPub-related settings", children: [ @@ -1814,7 +1875,7 @@ 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)." + "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, @@ -1827,6 +1888,7 @@ %{ group: :pleroma, key: :http_security, + label: "HTTP security", type: :group, description: "HTTP security settings", children: [ @@ -1865,7 +1927,7 @@ key: :report_uri, label: "Report URI", type: :string, - description: "Adds the specified url to report-uri and report-to group in CSP header", + description: "Adds the specified URL to report-uri and report-to group in CSP header", suggestions: ["https://example.com/report-uri"] } ] @@ -1873,9 +1935,10 @@ %{ 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", + "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it.", children: [ %{ key: :subject, @@ -1942,15 +2005,18 @@ }, %{ group: :pleroma, + label: "Pleroma Admin Token", type: :group, description: - "Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter", + "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: "Token", - suggestions: ["We recommend a secure random string or UUID"] + description: "Admin token", + suggestions: [ + "Please use a high entropy string or UUID" + ] } ] }, @@ -1968,18 +2034,11 @@ """, children: [ %{ - key: :verbose, + key: :log, type: {:dropdown, :atom}, description: "Logs verbose mode", suggestions: [false, :error, :warn, :info, :debug] }, - %{ - key: :prune, - type: [:atom, :tuple], - description: - "Non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning)", - suggestions: [:disabled, {:maxlen, 1500}, {:maxage, 60 * 60}] - }, %{ key: :queues, type: {:keyword, :integer}, @@ -2114,24 +2173,24 @@ key: :rich_media, type: :group, description: - "If enabled the instance will parse metadata from attached links to generate link previews.", + "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." + description: "Enables RichMedia parsing of URLs" }, %{ key: :ignore_hosts, type: {:list, :string}, - description: "List of hosts which will be ignored by the metadata parser.", + 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.", + description: "List TLDs (top-level domains) which will ignore for parse metadata", suggestions: ["local", "localdomain", "lan"] }, %{ @@ -2159,6 +2218,7 @@ %{ group: :pleroma, key: Pleroma.Formatter, + label: "Auto Linker", type: :group, description: "Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.", @@ -2248,6 +2308,7 @@ }, %{ group: :pleroma, + label: "Pleroma Authenticator", type: :group, description: "Authenticator", children: [ @@ -2261,6 +2322,7 @@ %{ 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" <> @@ -2347,6 +2409,7 @@ }, %{ key: :uid, + label: "UID", type: :string, description: "LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"", @@ -2362,11 +2425,12 @@ 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)," <> + "(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." }, %{ @@ -2378,6 +2442,7 @@ }, %{ 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" <> @@ -2386,6 +2451,7 @@ }, %{ 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." <> @@ -2459,7 +2525,7 @@ %{ key: :styling, type: :map, - description: "a map with color settings for email templates.", + description: "A map with color settings for email templates.", suggestions: [ %{ link_color: "#d8a070", @@ -2514,14 +2580,14 @@ %{ key: :enabled, type: :boolean, - description: "enables new users admin digest email when `true`", - suggestions: [false] + description: "Enables new users admin digest email when `true`" } ] }, %{ group: :pleroma, key: :oauth2, + label: "OAuth2", type: :group, description: "Configure OAuth 2 provider capabilities", children: [ @@ -2540,7 +2606,7 @@ %{ key: :clean_expired_tokens, type: :boolean, - description: "Enable a background job to clean expired oauth tokens. Default: disabled." + description: "Enable a background job to clean expired OAuth tokens. Default: disabled." } ] }, @@ -2564,7 +2630,7 @@ }, %{ key: :groups, - type: {:keyword, :string, {:list, :string}}, + 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.", @@ -2624,6 +2690,7 @@ }, %{ 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}]] @@ -2637,6 +2704,7 @@ }, %{ 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", @@ -2652,6 +2720,7 @@ }, %{ group: :esshd, + label: "ESSHD", type: :group, description: "Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <> @@ -2690,8 +2759,9 @@ }, %{ group: :mime, + label: "Mime Types", type: :group, - description: "Mime types", + description: "Mime Types settings", children: [ %{ key: :types, @@ -2750,6 +2820,7 @@ %{ group: :pleroma, key: :http, + label: "HTTP", type: :group, description: "HTTP settings", children: [ @@ -2798,6 +2869,7 @@ %{ group: :pleroma, key: :markup, + label: "Markup Settings", type: :group, children: [ %{ @@ -2838,8 +2910,9 @@ }, %{ group: :pleroma, + tab: :mrf, key: :mrf_normalize_markup, - label: "MRF normalize markup", + label: "MRF Normalize Markup", description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.", type: :group, children: [ @@ -2895,6 +2968,7 @@ }, %{ group: :cors_plug, + label: "CORS plug config", type: :group, children: [ %{ @@ -2967,6 +3041,7 @@ %{ 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.", @@ -2989,9 +3064,10 @@ %{ 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.", + "Render profiles and posts using server-generated HTML that is viewable without using JavaScript", children: [ %{ key: :enabled, @@ -3009,18 +3085,18 @@ %{ key: :post_title, type: :map, - description: "Configure title rendering.", + description: "Configure title rendering", children: [ %{ key: :max_length, type: :integer, - description: "Maximum number of characters before truncating title.", + description: "Maximum number of characters before truncating title", suggestions: [100] }, %{ key: :omission, type: :string, - description: "Replacement which will be used after truncating string.", + description: "Replacement which will be used after truncating string", suggestions: ["..."] } ] @@ -3030,8 +3106,11 @@ %{ group: :pleroma, key: :mrf_object_age, + label: "MRF Object Age", + tab: :mrf, type: :group, - description: "Rejects or delists posts based on their age when received.", + description: + "Rejects or delists posts based on their timestamp deviance from your server's clock.", children: [ %{ key: :threshold, @@ -3044,7 +3123,7 @@ type: {:list, :atom}, description: "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; " <> + "`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <> "`:reject` rejects the message entirely", suggestions: [:delist, :strip_followers, :reject] } @@ -3072,13 +3151,13 @@ %{ key: :workers, type: :integer, - description: "Number of workers to send notifications.", + description: "Number of workers to send notifications", suggestions: [3] }, %{ key: :overflow_workers, type: :integer, - description: "Maximum number of workers created if pool is empty.", + description: "Maximum number of workers created if pool is empty", suggestions: [2] } ] @@ -3090,36 +3169,37 @@ description: "Advanced settings for `gun` connections pool", children: [ %{ - key: :checkin_timeout, + key: :connection_acquisition_wait, type: :integer, - description: "Timeout to checkin connection from pool. Default: 250ms.", + 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: :retry, - type: :integer, - description: - "Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.", - suggestions: [1] - }, - %{ - key: :retry_timeout, - type: :integer, - description: - "Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.", - suggestions: [1000] - }, %{ key: :await_up_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] } ] }, @@ -3128,108 +3208,29 @@ key: :pools, type: :group, description: "Advanced settings for `gun` workers pools", - children: [ - %{ - key: :federation, - type: :keyword, - description: "Settings for federation pool.", - children: [ - %{ - key: :size, - type: :integer, - description: "Number workers in the pool.", - suggestions: [50] - }, - %{ - key: :max_overflow, - type: :integer, - description: "Number of additional workers if pool is under load.", - suggestions: [10] - }, - %{ - key: :timeout, - type: :integer, - description: "Timeout while `gun` will wait for response.", - suggestions: [150_000] - } - ] - }, - %{ - key: :media, - type: :keyword, - description: "Settings for media pool.", - children: [ - %{ - key: :size, - type: :integer, - description: "Number workers in the pool.", - suggestions: [50] - }, - %{ - key: :max_overflow, - type: :integer, - description: "Number of additional workers if pool is under load.", - suggestions: [10] - }, - %{ - key: :timeout, - type: :integer, - description: "Timeout while `gun` will wait for response.", - suggestions: [150_000] - } - ] - }, - %{ - key: :upload, - type: :keyword, - description: "Settings for upload pool.", - children: [ - %{ - key: :size, - type: :integer, - description: "Number workers in the pool.", - suggestions: [25] - }, - %{ - key: :max_overflow, - type: :integer, - description: "Number of additional workers if pool is under load.", - suggestions: [5] - }, - %{ - key: :timeout, - type: :integer, - description: "Timeout while `gun` will wait for response.", - suggestions: [300_000] - } - ] - }, - %{ - key: :default, - type: :keyword, - description: "Settings for default pool.", - children: [ - %{ - key: :size, - type: :integer, - description: "Number workers in the pool.", - suggestions: [10] - }, - %{ - key: :max_overflow, - type: :integer, - description: "Number of additional workers if pool is under load.", - suggestions: [2] - }, - %{ - key: :timeout, - type: :integer, - description: "Timeout while `gun` will wait for response.", - suggestions: [10_000] - } - ] - } - ] + 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 errrors when a new request is made", + suggestions: [10] + } + ] + } + end) }, %{ group: :pleroma, @@ -3365,44 +3366,46 @@ key: :strict, type: :boolean, description: - "Enables strict input validation (useful in development, not recommended in production)", - suggestions: [false] + "Enables strict input validation (useful in development, not recommended in production)" } ] }, %{ group: :pleroma, - key: :mrf, + key: :instances_favicons, type: :group, - description: "General MRF settings", + description: "Control favicons for instances", children: [ %{ - key: :policies, - type: [:module, {:list, :module}], - description: - "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.", - suggestions: - Generator.list_modules_in_dir( - "lib/pleroma/web/activity_pub/mrf", - "Elixir.Pleroma.Web.ActivityPub.MRF." - ) - }, - %{ - key: :transparency, - label: "MRF transparency", + key: :enabled, type: :boolean, - description: - "Make the content of your Message Rewrite Facility settings public (via nodeinfo)" + 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: :transparency_exclusions, - label: "MRF transparency exclusions", - type: {:list, :string}, - description: - "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", - suggestions: [ - "exclusion.com" - ] + 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"] } ] } diff --git a/config/test.exs b/config/test.exs index e38b9967d..abcf793e5 100644 --- a/config/test.exs +++ b/config/test.exs @@ -79,8 +79,8 @@ config :pleroma, Oban, queues: false, - prune: :disabled, - crontab: false + crontab: false, + plugins: false config :pleroma, Pleroma.ScheduledActivity, daily_user_limit: 2, @@ -111,6 +111,13 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true +config :pleroma, :instances_favicons, enabled: true + +config :pleroma, Pleroma.Uploaders.S3, + bucket: nil, + streaming_enabled: true, + public_endpoint: nil + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index be3c802af..c4a9c6dad 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -27,6 +27,7 @@ Has these additional fields under the `pleroma` object: - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `thread_muted`: true if the thread the post belongs to is muted - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. +- `parent_visible`: If the parent of this post is visible to the user or not. ## Media Attachments @@ -51,21 +52,27 @@ The `id` parameter can also be the `nickname` of the user. This only works in th Has these additional fields under the `pleroma` object: +- `ap_id`: nullable URL string, ActivityPub id of the user +- `background_image`: nullable URL string, background image of the user - `tags`: Lists an array of tags for the user -- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/ +- `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/ - `is_moderator`: boolean, nullable, true if user is a moderator - `is_admin`: boolean, nullable, true if user is an admin - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated +- `hide_favorites`: boolean, true when the user has hiding favorites enabled - `hide_followers`: boolean, true when the user has follower hiding enabled - `hide_follows`: boolean, true when the user has follow hiding enabled - `hide_followers_count`: boolean, true when the user has follower stat hiding enabled - `hide_follows_count`: boolean, true when the user has follow stat hiding enabled -- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` -- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` +- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials` +- `chat_token`: The token needed for Pleroma chat. Only returned in `/api/v1/accounts/verify_credentials` - `deactivated`: boolean, true when the user is deactivated - `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_notifications_count`: The count of unread notifications. Only returned to the account owner. +- `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned. +- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user +- `favicon`: nullable URL string, Favicon image of the user's instance ### Source @@ -162,7 +169,7 @@ Returns: array of Status. The maximum number of statuses is limited to 100 per request. -## PATCH `/api/v1/update_credentials` +## PATCH `/api/v1/accounts/update_credentials` Additional parameters can be added to the JSON body/Form data: @@ -177,9 +184,12 @@ Additional parameters can be added to the JSON body/Form data: - `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `skip_thread_containment` - if true, skip filtering out broken threads - `allow_following_move` - if true, allows automatically follow moved following accounts -- `pleroma_background_image` - sets the background image of the user. +- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset. - `discoverable` - if true, discovery of this account in search results and other services is allowed. - `actor_type` - the type of this account. +- `accepts_chat_messages` - if false, this account will reject all chat messages. + +All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file. ### Pleroma Settings Store @@ -187,7 +197,7 @@ Pleroma has mechanism that allows frontends to save blobs of json for each user The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings. -This information is returned in the `verify_credentials` endpoint. +This information is returned in the `/api/v1/accounts/verify_credentials` endpoint. ## Authentication @@ -215,6 +225,8 @@ Has theses additional parameters (which are the same as in Pleroma-API): `GET /api/v1/instance` has additional fields - `max_toot_chars`: The maximum characters per post +- `chat_limit`: The maximum characters per chat message +- `description_limit`: The maximum characters per image description - `poll_limits`: The limits of polls - `upload_limit`: The maximum upload file size - `avatar_upload_limit`: The same for avatars @@ -223,6 +235,7 @@ Has theses additional parameters (which are the same as in Pleroma-API): - `background_image`: A background image that frontends can use - `pleroma.metadata.features`: A list of supported features - `pleroma.metadata.federation`: The federation restrictions of this instance +- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields. - `vapid_public_key`: The public key needed for push messages ## Markers @@ -234,3 +247,43 @@ Has these additional fields under the `pleroma` object: ## Streaming There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. + +## Not implemented + +Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority. + +### Suggestions + +*Added in Mastodon 2.4.3* + +- `GET /api/v1/suggestions`: Returns an empty array, `[]` + +### Trends + +*Added in Mastodon 3.0.0* + +- `GET /api/v1/trends`: Returns an empty array, `[]` + +### Identity proofs + +*Added in Mastodon 2.8.0* + +- `GET /api/v1/identity_proofs`: Returns an empty array, `[]` + +### Endorsements + +*Added in Mastodon 2.5.0* + +- `GET /api/v1/endorsements`: Returns an empty array, `[]` + +### Profile directory + +*Added in Mastodon 3.0.0* + +- `GET /api/v1/directory`: Returns HTTP 404 + +### Featured tags + +*Added in Mastodon 3.0.0* + +- `GET /api/v1/featured_tags`: Returns HTTP 404 diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index b7eee5192..5bd38ad36 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -287,11 +287,8 @@ See [Admin-API](admin_api.md) * Method `PUT` * Authentication: required * Params: - * `followers`: BOOLEAN field, receives notifications from followers - * `follows`: BOOLEAN field, receives notifications from people the user follows - * `remote`: BOOLEAN field, receives notifications from people on remote instances - * `local`: BOOLEAN field, receives notifications from people on the local instance - * `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification. + * `block_from_strangers`: BOOLEAN field, blocks notifications from accounts you do not follow + * `hide_notification_contents`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification. * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` ## `/api/pleroma/healthcheck` diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index 1e6f4a8b4..3b4c421a7 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -57,11 +57,11 @@ mix pleroma.user invites ## Revoke invite ```sh tab="OTP" - ./bin/pleroma_ctl user revoke_invite + ./bin/pleroma_ctl user revoke_invite ``` ```sh tab="From Source" -mix pleroma.user revoke_invite +mix pleroma.user revoke_invite ``` diff --git a/docs/administration/updating.md b/docs/administration/updating.md index 2a08dac1f..c994f3f16 100644 --- a/docs/administration/updating.md +++ b/docs/administration/updating.md @@ -1,6 +1,6 @@ # Updating your instance -You should **always check the release notes/changelog** in case there are config deprecations, special update special update steps, etc. +You should **always check the [release notes/changelog](https://git.pleroma.social/pleroma/pleroma/-/releases)** in case there are config deprecations, special update steps, etc. Besides that, doing the following is generally enough: diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 22b28d423..042ad30c9 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -18,6 +18,7 @@ To add configuration to your config file, you can copy it from the base config. * `notify_email`: Email used for notifications. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). +* `discription_limit`: The character limit for image descriptions. * `chat_limit`: Character limit of the instance chat messages. * `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `upload_limit`: File size limit of uploads (except for avatar, background, banner). @@ -36,7 +37,7 @@ To add configuration to your config file, you can copy it from the base config. * `federation_incoming_replies_max_depth`: Max. depth of reply-to 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. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. * `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance. -* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. +* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. See also: `restrict_unauthenticated`. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). @@ -154,7 +155,7 @@ config :pleroma, :mrf_user_allowlist, %{ * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines * `:reject` rejects the message entirely -#### mrf_steal_emoji +#### :mrf_steal_emoji * `hosts`: List of hosts to steal emojis from * `rejected_shortcodes`: Regex-list of shortcodes to reject * `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk @@ -251,6 +252,7 @@ This section describe PWA manifest instance-specific values. Currently this opti * `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`). ## :emoji + * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]` * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` @@ -259,13 +261,14 @@ This section describe PWA manifest instance-specific values. Currently this opti memory for this amount of seconds multiplied by the number of files. ## :media_proxy + * `enabled`: Enables proxying of remote media to the instance’s proxy * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`. -* `whitelist`: List of domains to bypass the mediaproxy +* `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`) * `invalidation`: options for remove media from cache after delete object: - * `enabled`: Enables purge cache - * `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use. + * `enabled`: Enables purge cache + * `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use. ### Purge cache strategy @@ -277,6 +280,7 @@ Urls of attachments pass to script as arguments. * `script_path`: path to external script. Example: + ```elixir config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: "./installation/nginx-cache-purge.example" @@ -444,37 +448,32 @@ For each pool, the options are: *For `gun` adapter* -Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools. +Settings for HTTP connection pool. -For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000. -It will increase memory usage, but federation would work faster. - -* `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms. -* `:max_connections` - maximum number of connections in the pool. Default: 250 connections. -* `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1. -* `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms. -* `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms. +* `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. +* `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart. +* `:max_connections` - Maximum number of connections in the pool. +* `:await_up_timeout` - Timeout to connect to the host. +* `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded. ### :pools *For `gun` adapter* -Advanced settings for workers pools. +Settings for request pools. These pools are limited on top of `:connections_pool`. There are four pools used: -* `:federation` for the federation jobs. - You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs. -* `:media` for rich media, media proxy -* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`) -* `:default` for other requests +* `:federation` for the federation jobs. You may want this pool's max_connections to be at least equal to the number of federator jobs + retry queue jobs. +* `:media` - for rich media, media proxy. +* `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`. +* `:default` - for other requests. For each pool, the options are: -* `:size` - how much workers the pool can hold +* `:size` - limit to how much requests can be concurrently executed. * `:timeout` - timeout while `gun` will wait for response -* `:max_overflow` - additional workers if pool is under load - +* `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped. ## Captcha @@ -493,7 +492,7 @@ A built-in captcha provider. Enabled by default. #### Pleroma.Captcha.Kocaptcha 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 +the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). The default endpoint `https://captcha.kotobank.ch` is hosted by the developer. * `endpoint`: the Kocaptcha endpoint to use. @@ -501,6 +500,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end ## Uploads ### Pleroma.Upload + * `uploader`: Which one of the [uploaders](#uploaders) to use. * `filters`: List of [upload filters](#upload-filters) to use. * `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` @@ -513,10 +513,15 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. ### Uploaders + #### Pleroma.Uploaders.Local + * `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory. #### Pleroma.Uploaders.S3 + +Don't forget to configure [Ex AWS S3](#ex-aws-s3-settings) + * `bucket`: S3 bucket name. * `bucket_namespace`: S3 bucket namespace. * `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com") @@ -525,17 +530,23 @@ For example, when using CDN to S3 virtual host format, set "". At this time, write CNAME to CDN in public_endpoint. * `streaming_enabled`: 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. +#### Ex AWS S3 settings + +* `access_key_id`: Access key ID +* `secret_access_key`: Secret access key +* `host`: S3 host + +Example: + +```elixir +config :ex_aws, :s3, + access_key_id: "xxxxxxxxxx", + secret_access_key: "yyyyyyyyyy", + host: "s3.eu-central-1.amazonaws.com" +``` ### Upload filters -#### Pleroma.Upload.Filter.Mogrify - -* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`. - -#### Pleroma.Upload.Filter.Dedupe - -No specific configuration. - #### Pleroma.Upload.Filter.AnonymizeFilename This filter replaces the filename (not the path) of an upload. For complete obfuscation, add @@ -543,6 +554,20 @@ This filter replaces the filename (not the path) of an upload. For complete obfu * `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`. +#### Pleroma.Upload.Filter.Dedupe + +No specific configuration. + +#### Pleroma.Upload.Filter.Exiftool + +This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact. + +No specific configuration. + +#### Pleroma.Upload.Filter.Mogrify + +* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`. + ## Email ### Pleroma.Emails.Mailer @@ -603,8 +628,7 @@ Email notifications settings. Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage): * `repo` - app's Ecto repo (`Pleroma.Repo`) -* `verbose` - logs verbosity -* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`) +* `log` - logs verbosity * `queues` - job queues (see below) * `crontab` - periodic jobs, see [`Oban.Cron`](#obancron) @@ -789,6 +813,8 @@ or curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites" ``` +Warning: it's discouraged to use this feature because of the associated security risk: static / rarely changed instance-wide token is much weaker compared to email-password pair of a real admin user; consider using HTTP Basic Auth or OAuth-based authentication instead. + ### :auth * `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator. @@ -969,11 +995,11 @@ config :pleroma, :database_config_whitelist, [ ### :restrict_unauthenticated -Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. +Restrict access for unauthenticated users to timelines (public and federated), user profiles and statuses. * `timelines`: public and federated timelines * `local`: public timeline - * `federated` + * `federated`: federated timeline (includes public timeline) * `profiles`: user profiles * `local` * `remote` @@ -981,7 +1007,14 @@ Restrict access for unauthenticated users to timelines (public and federate), us * `local` * `remote` +Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practical sense if `restrict_unauthenticated/timelines/federated` is set to `false` (since local public activities will still be delivered to unauthenticated users as part of federated timeline). ## Pleroma.Web.ApiSpec.CastAndValidate * `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`. + +## :instances_favicons + +Control favicons for instances. + +* `enabled`: Allow/disallow displaying and getting instances favicons diff --git a/docs/configuration/howto_database_config.md b/docs/configuration/howto_database_config.md new file mode 100644 index 000000000..ded9a2eb3 --- /dev/null +++ b/docs/configuration/howto_database_config.md @@ -0,0 +1,151 @@ +# How to activate Pleroma in-database configuration +## Explanation + +The configuration of Pleroma has traditionally been managed with a config file, e.g. `config/prod.secret.exs`. This method requires a restart of the application for any configuration changes to take effect. We have made it possible to control most settings in the AdminFE interface after running a migration script. + +## Migration to database config + +1. Stop your Pleroma instance and edit your Pleroma config to enable database configuration: + + ``` + config :pleroma, configurable_from_database: true + ``` + +2. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened. + + **Source:** + + ``` + $ mix pleroma.config migrate_to_db + ``` + + or + + **OTP:** + + ``` + $ ./bin/pleroma_ctl config migrate_to_db + ``` + + ``` + 10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms +SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 [] +Migrating settings from file: /home/pleroma/config/dev.secret.exs + + 10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms +TRUNCATE config; [] + + 10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms +ALTER SEQUENCE config_id_seq RESTART; [] + + 10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms +SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"] + + 10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms +INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]] + Settings for key instance migrated. + Settings for group :pleroma migrated. + ``` + +3. It is recommended to backup your config file now. + ``` + cp config/dev.secret.exs config/dev.secret.exs.orig + ``` + +4. Now you can edit your config file and strip it down to the only settings which are not possible to control in the database. e.g., the Postgres and webserver (Endpoint) settings cannot be controlled in the database because the application needs the settings to start up and access the database. + + ⚠️ **THIS IS NOT REQUIRED** + + Any settings in the database will override those in the config file, but you may find it less confusing if the setting is only declared in one place. + + A non-exhaustive list of settings that are only possible in the config file include the following: + +* config :pleroma, Pleroma.Web.Endpoint +* config :pleroma, Pleroma.Repo +* config :pleroma, configurable_from_database +* config :pleroma, :database, rum_enabled +* config :pleroma, :connections_pool + +Here is an example of a server config stripped down after migration: + +``` +use Mix.Config + +config :pleroma, Pleroma.Web.Endpoint, + url: [host: "cool.pleroma.site", scheme: "https", port: 443] + + +config :pleroma, Pleroma.Repo, + adapter: Ecto.Adapters.Postgres, + username: "pleroma", + password: "MySecretPassword", + database: "pleroma_prod", + hostname: "localhost" + +config :pleroma, configurable_from_database: true +``` + +5. Start your instance back up and you can now access the Settings tab in AdminFE. + + +## Reverting back from database config + +1. Stop your Pleroma instance. + +2. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened. + + **Source:** + + ``` + $ mix pleroma.config migrate_from_db + ``` + + or + + **OTP:** + + ``` + $ ./bin/pleroma_ctl config migrate_from_db + ``` + + ``` + 10:26:30.593 [debug] QUERY OK source="config" db=9.8ms decode=1.2ms queue=26.0ms idle=0.0ms +SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 [] + + 10:26:30.659 [debug] QUERY OK source="config" db=1.1ms idle=80.7ms +SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 [] +Database configuration settings have been saved to config/dev.exported_from_db.secret.exs +``` + +3. The in-database configuration still exists, but it will not be used if you remove `config :pleroma, configurable_from_database: true` from your config. + +## Debugging + +### Clearing database config +You can clear the database config by truncating the `config` table in the database. e.g., + +``` +psql -d pleroma_dev +pleroma_dev=# TRUNCATE config; +TRUNCATE TABLE +``` + +Additionally, every time you migrate the configuration to the database the config table is automatically truncated to ensure a clean migration. + +### Manually removing a setting +If you encounter a situation where the server cannot run properly because of an invalid setting in the database and this is preventing you from accessing AdminFE, you can manually remove the offending setting if you know which one it is. + +e.g., here is an example showing a minimal configuration in the database. Only the `config :pleroma, :instance` settings are in the table: + +``` +psql -d pleroma_dev +pleroma_dev=# select * from config; + id | key | value | inserted_at | updated_at | group +----+-----------+------------------------------------------------------------+---------------------+---------------------+---------- + 1 | :instance | \x836c0000000168026400046e616d656d00000007426c65726f6d616a | 2020-07-12 15:33:29 | 2020-07-12 15:33:29 | :pleroma +(1 row) +pleroma_dev=# delete from config where key = ':instance' and group = ':pleroma'; +DELETE 1 +``` + +Now the `config :pleroma, :instance` settings have been removed from the database. diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 3ad6edbfb..9f0bf6ecb 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -3,15 +3,48 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Pleroma do + @apps [ + :restarter, + :ecto, + :ecto_sql, + :postgrex, + :db_connection, + :cachex, + :flake_id, + :swoosh, + :timex + ] + @cachex_children ["object", "user"] @doc "Common functions to be reused in mix tasks" def start_pleroma do + Pleroma.Config.Holder.save_default() Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) if Pleroma.Config.get(:env) != :test do Application.put_env(:logger, :console, level: :debug) end - {:ok, _} = Application.ensure_all_started(:pleroma) + apps = + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do + [:gun | @apps] + else + [:hackney | @apps] + end + + Enum.each(apps, &Application.ensure_all_started/1) + + children = [ + Pleroma.Repo, + {Pleroma.Config.TransferTask, false}, + Pleroma.Web.Endpoint + ] + + cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, [])) + + Supervisor.start_link(children ++ cachex_children, + strategy: :one_for_one, + name: Pleroma.Supervisor + ) if Pleroma.Config.get(:env) not in [:test, :benchmark] do pleroma_rebooted?() diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index d5129d410..904c5a74b 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -83,7 +83,7 @@ defp create(group, settings) do defp migrate_from_db(opts) do if Pleroma.Config.get([:configurable_from_database]) do - env = opts[:env] || "prod" + env = opts[:env] || Pleroma.Config.get(:env) config_path = if Pleroma.Config.get(:release) do @@ -105,6 +105,10 @@ defp migrate_from_db(opts) do :ok = File.close(file) System.cmd("mix", ["format", config_path]) + + shell_info( + "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs" + ) else migration_error() end @@ -112,7 +116,7 @@ defp migrate_from_db(opts) do defp migration_error do shell_error( - "Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true." + "Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`" ) end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 86409738a..91440b453 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -145,7 +145,7 @@ def run(["gen" | rest]) do options, :uploads_dir, "What directory should media uploads go in (when using the local uploader)?", - Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads]) + Config.get([Pleroma.Uploaders.Local, :uploads]) ) |> Path.expand() @@ -154,7 +154,7 @@ def run(["gen" | rest]) do options, :static_dir, "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", - Pleroma.Config.get([:instance, :static_dir]) + Config.get([:instance, :static_dir]) ) |> Path.expand() diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex index 7d65f0587..00f5ba7bf 100644 --- a/lib/mix/tasks/pleroma/notification_settings.ex +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do @moduledoc """ Example: - > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user - > mix pleroma.notification_settings --privacy-option=true # set true for all users + > mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588" # set false only for parallel588 user + > mix pleroma.notification_settings --hide-notification-contents=true # set true for all users """ @@ -19,16 +19,16 @@ def run(args) do OptionParser.parse( args, strict: [ - privacy_option: :boolean, + hide_notification_contents: :boolean, email_users: :string, nickname_users: :string ] ) - privacy_option = Keyword.get(options, :privacy_option) + hide_notification_contents = Keyword.get(options, :hide_notification_contents) - if not is_nil(privacy_option) do - privacy_option + if not is_nil(hide_notification_contents) do + hide_notification_contents |> build_query(options) |> Pleroma.Repo.update_all([]) end @@ -36,15 +36,15 @@ def run(args) do shell_info("Done") end - defp build_query(privacy_option, options) do + defp build_query(hide_notification_contents, options) do query = from(u in Pleroma.User, update: [ set: [ notification_settings: fragment( - "jsonb_set(notification_settings, '{privacy_option}', ?)", - ^privacy_option + "jsonb_set(notification_settings, '{hide_notification_contents}', ?)", + ^hide_notification_contents ) ] ] diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bca7e87bf..01824aa18 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -232,7 +232,7 @@ def run(["tag", nickname | tags]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do user = user |> User.tag(tags) - shell_info("Tags of #{user.nickname}: #{inspect(tags)}") + shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}") else _ -> shell_error("Could not change user tags for #{nickname}") @@ -245,7 +245,7 @@ def run(["untag", nickname | tags]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do user = user |> User.untag(tags) - shell_info("Tags of #{user.nickname}: #{inspect(tags)}") + shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}") else _ -> shell_error("Could not change user tags for #{nickname}") diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9615af122..0ffb55358 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -35,13 +35,19 @@ def user_agent do # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do - Pleroma.Config.Holder.save_default() + # Scrubbers are compiled at runtime and therefore will cause a conflict + # every time the application is restarted, so we disable module + # conflicts at runtime + Code.compiler_options(ignore_module_conflict: true) + Pleroma.Telemetry.Logger.attach() + Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() Config.DeprecationWarnings.warn() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.ApplicationRequirements.verify!() setup_instrumenters() load_custom_modules() + Pleroma.Docs.JSON.compile() adapter = Application.get_env(:tesla, :adapter) @@ -162,7 +168,8 @@ defp idempotency_expiration, defp seconds_valid_interval, do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) - defp build_cachex(type, opts), + @spec build_cachex(String.t(), keyword()) :: map() + def build_cachex(type, opts), do: %{ id: String.to_atom("cachex_" <> type), start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, @@ -217,9 +224,7 @@ defp task_children(_) do # start hackney and gun pools in tests defp http_children(_, :test) do - hackney_options = Config.get([:hackney_pools, :federation]) - hackney_pool = :hackney_pool.child_spec(:federation, hackney_options) - [hackney_pool, Pleroma.Pool.Supervisor] + http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil) end defp http_children(Tesla.Adapter.Hackney, _) do @@ -238,7 +243,10 @@ defp http_children(Tesla.Adapter.Hackney, _) do end end - defp http_children(Tesla.Adapter.Gun, _), do: [Pleroma.Pool.Supervisor] + defp http_children(Tesla.Adapter.Gun, _) do + Pleroma.Gun.ConnectionPool.children() ++ + [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}] + end defp http_children(_, _), do: [] end diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 0a6c724fb..026871c4f 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -54,6 +54,7 @@ def warn do check_hellthread_threshold() mrf_user_allowlist() check_old_mrf_config() + check_media_proxy_whitelist_config() end def check_old_mrf_config do @@ -65,7 +66,7 @@ def check_old_mrf_config do move_namespace_and_warn(@mrf_config_map, warning_preface) end - @spec move_namespace_and_warn([config_map()], String.t()) :: :ok + @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil def move_namespace_and_warn(config_map, warning_preface) do warning = Enum.reduce(config_map, "", fn @@ -84,4 +85,16 @@ def move_namespace_and_warn(config_map, warning_preface) do Logger.warn(warning_preface <> warning) end end + + @spec check_media_proxy_whitelist_config() :: :ok | nil + def check_media_proxy_whitelist_config do + whitelist = Config.get([:media_proxy, :whitelist]) + + if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later. + """) + end + end end diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 0f3ecf1ed..64e7de6df 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do :swarm ] + @reject_groups [ + :postgrex, + :tesla + ] + if Code.ensure_loaded?(Config.Reader) do @reader Config.Reader @@ -47,7 +52,8 @@ defp filter(configs) do @spec filter_group(atom(), keyword()) :: keyword() def filter_group(group, configs) do Enum.reject(configs[group], fn {key, _v} -> - key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex + key in @reject_keys or group in @reject_groups or + (group == :phoenix and key == :serve_endpoints) end) end end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index eb86b8ff4..a0d7b7d71 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do {:pleroma, :gopher, [:enabled]} ] - def start_link(_) do - load_and_update_env() + def start_link(restart_pleroma? \\ true) do + load_and_update_env([], restart_pleroma?) if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) :ignore end diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex index e0fc8cd02..a671a6278 100644 --- a/lib/pleroma/docs/generator.ex +++ b/lib/pleroma/docs/generator.ex @@ -6,16 +6,21 @@ def process(implementation, descriptions) do implementation.process(descriptions) end - @spec list_modules_in_dir(String.t(), String.t()) :: [module()] - def list_modules_in_dir(dir, start) do - with {:ok, files} <- File.ls(dir) do - files - |> Enum.filter(&String.ends_with?(&1, ".ex")) - |> Enum.map(fn filename -> - module = filename |> String.trim_trailing(".ex") |> Macro.camelize() - String.to_atom(start <> module) - end) - end + @spec list_behaviour_implementations(behaviour :: module()) :: [module()] + def list_behaviour_implementations(behaviour) do + :code.all_loaded() + |> Enum.filter(fn {module, _} -> + # This shouldn't be needed as all modules are expected to have module_info/1, + # but in test enviroments some transient modules `:elixir_compiler_XX` + # are loaded for some reason (where XX is a random integer). + if function_exported?(module, :module_info, 1) do + module.module_info(:attributes) + |> Keyword.get_values(:behaviour) + |> List.flatten() + |> Enum.member?(behaviour) + end + end) + |> Enum.map(fn {module, _} -> module end) end @doc """ @@ -87,6 +92,12 @@ defp humanize(entity) do else: string end + defp format_suggestions({:list_behaviour_implementations, behaviour}) do + behaviour + |> list_behaviour_implementations() + |> format_suggestions() + end + defp format_suggestions([]), do: [] defp format_suggestions([suggestion | tail]) do diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex index d1cf1f487..feeb4320e 100644 --- a/lib/pleroma/docs/json.ex +++ b/lib/pleroma/docs/json.ex @@ -1,5 +1,19 @@ defmodule Pleroma.Docs.JSON do @behaviour Pleroma.Docs.Generator + @external_resource "config/description.exs" + @raw_config Pleroma.Config.Loader.read("config/description.exs") + @raw_descriptions @raw_config[:pleroma][:config_description] + @term __MODULE__.Compiled + + @spec compile :: :ok + def compile do + :persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions)) + end + + @spec compiled_descriptions :: Map.t() + def compiled_descriptions do + :persistent_term.get(@term) + end @spec process(keyword()) :: {:ok, String.t()} def process(descriptions) do @@ -13,11 +27,4 @@ def process(descriptions) do {:ok, path} end end - - def compile do - with config <- Pleroma.Config.Loader.read("config/description.exs") do - config[:pleroma][:config_description] - |> Pleroma.Docs.Generator.convert_to_strings() - end - end end diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex index 68b106499..da3f20f43 100644 --- a/lib/pleroma/docs/markdown.ex +++ b/lib/pleroma/docs/markdown.ex @@ -68,6 +68,11 @@ defp print_suggestion(file, suggestion, as_list \\ false) do IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n") end + defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do + suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour) + print_suggestions(file, suggestions) + end + defp print_suggestions(_file, nil), do: nil defp print_suggestions(_file, ""), do: nil diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index 55f61024e..aa0b2a66b 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do alias Pleroma.Config alias Pleroma.Web.Router.Helpers - defp instance_config, do: Pleroma.Config.get(:instance) + defp instance_config, do: Config.get(:instance) defp instance_name, do: instance_config()[:name] defp instance_notify_email do @@ -72,6 +72,8 @@ def report(to, reporter, account, statuses, comment) do

Reported Account: #{account.nickname}

#{comment_html} #{statuses_html} +

+ View Reports in AdminFE """ new() diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex index 3de2dc762..03a6bca0b 100644 --- a/lib/pleroma/emoji/loader.ex +++ b/lib/pleroma/emoji/loader.ex @@ -108,7 +108,7 @@ defp load_pack(pack_dir, emoji_groups) do if File.exists?(emoji_txt) do load_from_file(emoji_txt, emoji_groups) else - extensions = Pleroma.Config.get([:emoji, :pack_extensions]) + extensions = Config.get([:emoji, :pack_extensions]) Logger.info( "No emoji.txt found for pack \"#{pack_name}\", assuming all #{ diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 4d61b3650..5d6df9530 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do Repo.one(query) end - def get_filters(%User{id: user_id} = _user) do + def get_active(query) do + from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now()) + end + + def get_irreversible(query) do + from(f in query, where: f.hide) + end + + def get_filters(query \\ __MODULE__, %User{id: user_id}) do query = from( - f in Pleroma.Filter, + f in query, where: f.user_id == ^user_id, order_by: [desc: :id] ) @@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do |> validate_required([:phrase, :context]) |> Repo.update() end + + def compose_regex(user_or_filters, format \\ :postgres) + + def compose_regex(%User{} = user, format) do + __MODULE__ + |> get_active() + |> get_irreversible() + |> get_filters(user) + |> compose_regex(format) + end + + def compose_regex([_ | _] = filters, format) do + phrases = + filters + |> Enum.map(& &1.phrase) + |> Enum.join("|") + + case format do + :postgres -> + "\\y(#{phrases})\\y" + + :re -> + ~r/\b#{phrases}\b/i + + _ -> + nil + end + end + + def compose_regex(_, _), do: nil end diff --git a/lib/pleroma/gun/api.ex b/lib/pleroma/gun/api.ex index f51cd7db8..09be74392 100644 --- a/lib/pleroma/gun/api.ex +++ b/lib/pleroma/gun/api.ex @@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do :tls_opts, :tcp_opts, :socks_opts, - :ws_opts + :ws_opts, + :supervise ] @impl Gun diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index cd25a2e74..a3f75a4bb 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -3,85 +3,33 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Gun.Conn do - @moduledoc """ - Struct for gun connection data - """ alias Pleroma.Gun - alias Pleroma.Pool.Connections require Logger - @type gun_state :: :up | :down - @type conn_state :: :active | :idle - - @type t :: %__MODULE__{ - conn: pid(), - gun_state: gun_state(), - conn_state: conn_state(), - used_by: [pid()], - last_reference: pos_integer(), - crf: float(), - retries: pos_integer() - } - - defstruct conn: nil, - gun_state: :open, - conn_state: :init, - used_by: [], - last_reference: 0, - crf: 1, - retries: 0 - - @spec open(String.t() | URI.t(), atom(), keyword()) :: :ok | nil - def open(url, name, opts \\ []) - def open(url, name, opts) when is_binary(url), do: open(URI.parse(url), name, opts) - - def open(%URI{} = uri, name, opts) do + def open(%URI{} = uri, opts) do pool_opts = Pleroma.Config.get([:connections_pool], []) opts = opts |> Enum.into(%{}) - |> Map.put_new(:retry, pool_opts[:retry] || 1) - |> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000) |> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000) + |> Map.put_new(:supervise, false) |> maybe_add_tls_opts(uri) - key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - - max_connections = pool_opts[:max_connections] || 250 - - conn_pid = - if Connections.count(name) < max_connections do - do_open(uri, opts) - else - close_least_used_and_do_open(name, uri, opts) - end - - if is_pid(conn_pid) do - conn = %Pleroma.Gun.Conn{ - conn: conn_pid, - gun_state: :up, - conn_state: :active, - last_reference: :os.system_time(:second) - } - - :ok = Gun.set_owner(conn_pid, Process.whereis(name)) - Connections.add_conn(name, key, conn) - end + do_open(uri, opts) end defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts - defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do + defp maybe_add_tls_opts(opts, %URI{scheme: "https"}) do tls_opts = [ verify: :verify_peer, cacertfile: CAStore.file_path(), depth: 20, reuse_sessions: false, - verify_fun: - {&:ssl_verify_hostname.verify_fun/3, - [check_hostname: Pleroma.HTTP.Connection.format_host(host)]} + log_level: :warning, + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)] ] tls_opts = @@ -105,7 +53,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]), stream <- Gun.connect(conn, connect_opts), {:response, :fin, 200, _} <- Gun.await(conn, stream) do - conn + {:ok, conn} else error -> Logger.warn( @@ -141,7 +89,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts), {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do - conn + {:ok, conn} else error -> Logger.warn( @@ -155,11 +103,11 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do end defp do_open(%URI{host: host, port: port} = uri, opts) do - host = Pleroma.HTTP.Connection.parse_host(host) + host = Pleroma.HTTP.AdapterHelper.parse_host(host) with {:ok, conn} <- Gun.open(host, port, opts), {:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do - conn + {:ok, conn} else error -> Logger.warn( @@ -171,7 +119,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do end defp destination_opts(%URI{host: host, port: port}) do - host = Pleroma.HTTP.Connection.parse_host(host) + host = Pleroma.HTTP.AdapterHelper.parse_host(host) %{host: host, port: port} end @@ -181,17 +129,6 @@ defp add_http2_opts(opts, "https", tls_opts) do defp add_http2_opts(opts, _, _), do: opts - defp close_least_used_and_do_open(name, uri, opts) do - with [{key, conn} | _conns] <- Connections.get_unused_conns(name), - :ok <- Gun.close(conn.conn) do - Connections.remove_conn(name, key) - - do_open(uri, opts) - else - [] -> {:error, :pool_overflowed} - end - end - def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do "#{scheme}://#{host}#{path}" end diff --git a/lib/pleroma/gun/connection_pool.ex b/lib/pleroma/gun/connection_pool.ex new file mode 100644 index 000000000..8b41a668c --- /dev/null +++ b/lib/pleroma/gun/connection_pool.ex @@ -0,0 +1,79 @@ +defmodule Pleroma.Gun.ConnectionPool do + @registry __MODULE__ + + alias Pleroma.Gun.ConnectionPool.WorkerSupervisor + + def children do + [ + {Registry, keys: :unique, name: @registry}, + Pleroma.Gun.ConnectionPool.WorkerSupervisor + ] + end + + def get_conn(uri, opts) do + key = "#{uri.scheme}:#{uri.host}:#{uri.port}" + + case Registry.lookup(@registry, key) do + # The key has already been registered, but connection is not up yet + [{worker_pid, nil}] -> + get_gun_pid_from_worker(worker_pid, true) + + [{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] -> + GenServer.cast(worker_pid, {:add_client, self(), false}) + {:ok, gun_pid} + + [] -> + # :gun.set_owner fails in :connected state for whatevever reason, + # so we open the connection in the process directly and send it's pid back + # We trust gun to handle timeouts by itself + case WorkerSupervisor.start_worker([key, uri, opts, self()]) do + {:ok, worker_pid} -> + get_gun_pid_from_worker(worker_pid, false) + + {:error, {:already_started, worker_pid}} -> + get_gun_pid_from_worker(worker_pid, true) + + err -> + err + end + end + end + + defp get_gun_pid_from_worker(worker_pid, register) do + # GenServer.call will block the process for timeout length if + # the server crashes on startup (which will happen if gun fails to connect) + # so instead we use cast + monitor + + ref = Process.monitor(worker_pid) + if register, do: GenServer.cast(worker_pid, {:add_client, self(), true}) + + receive do + {:conn_pid, pid} -> + Process.demonitor(ref) + {:ok, pid} + + {:DOWN, ^ref, :process, ^worker_pid, reason} -> + case reason do + {:shutdown, error} -> error + _ -> {:error, reason} + end + end + end + + def release_conn(conn_pid) do + # :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid -> + # worker_pid end) + query_result = + Registry.select(@registry, [ + {{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]} + ]) + + case query_result do + [worker_pid] -> + GenServer.cast(worker_pid, {:remove_client, self()}) + + [] -> + :ok + end + end +end diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex new file mode 100644 index 000000000..cea800882 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -0,0 +1,85 @@ +defmodule Pleroma.Gun.ConnectionPool.Reclaimer do + use GenServer, restart: :temporary + + @registry Pleroma.Gun.ConnectionPool + + def start_monitor do + pid = + case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do + {:ok, pid} -> + pid + + {:error, {:already_registered, pid}} -> + pid + end + + {pid, Process.monitor(pid)} + end + + @impl true + def init(_) do + {:ok, nil, {:continue, :reclaim}} + end + + @impl true + def handle_continue(:reclaim, _) do + max_connections = Pleroma.Config.get([:connections_pool, :max_connections]) + + reclaim_max = + [:connections_pool, :reclaim_multiplier] + |> Pleroma.Config.get() + |> Kernel.*(max_connections) + |> round + |> max(1) + + :telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{ + max_connections: max_connections, + reclaim_max: reclaim_max + }) + + # :ets.fun2ms( + # fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] -> + # {worker_pid, crf, last_reference} end) + unused_conns = + Registry.select( + @registry, + [ + {{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]} + ] + ) + + case unused_conns do + [] -> + :telemetry.execute( + [:pleroma, :connection_pool, :reclaim, :stop], + %{reclaimed_count: 0}, + %{ + max_connections: max_connections + } + ) + + {:stop, :no_unused_conns, nil} + + unused_conns -> + reclaimed = + unused_conns + |> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} -> + crf1 <= crf2 and last_reference1 <= last_reference2 + end) + |> Enum.take(reclaim_max) + + reclaimed + |> Enum.each(fn {pid, _, _} -> + DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid) + end) + + :telemetry.execute( + [:pleroma, :connection_pool, :reclaim, :stop], + %{reclaimed_count: Enum.count(reclaimed)}, + %{max_connections: max_connections} + ) + + {:stop, :normal, nil} + end + end +end diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex new file mode 100644 index 000000000..f33447cb6 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -0,0 +1,127 @@ +defmodule Pleroma.Gun.ConnectionPool.Worker do + alias Pleroma.Gun + use GenServer, restart: :temporary + + @registry Pleroma.Gun.ConnectionPool + + def start_link([key | _] = opts) do + GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}}) + end + + @impl true + def init([_key, _uri, _opts, _client_pid] = opts) do + {:ok, nil, {:continue, {:connect, opts}}} + end + + @impl true + def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do + with {:ok, conn_pid} <- Gun.Conn.open(uri, opts), + Process.link(conn_pid) do + time = :erlang.monotonic_time(:millisecond) + + {_, _} = + Registry.update_value(@registry, key, fn _ -> + {conn_pid, [client_pid], 1, time} + end) + + send(client_pid, {:conn_pid, conn_pid}) + + {:noreply, + %{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}}, + :hibernate} + else + err -> + {:stop, {:shutdown, err}, nil} + end + end + + @impl true + def handle_cast({:add_client, client_pid, send_pid_back}, %{key: key} = state) do + time = :erlang.monotonic_time(:millisecond) + + {{conn_pid, _, _, _}, _} = + Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> + {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time} + end) + + if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid}) + + state = + if state.timer != nil do + Process.cancel_timer(state[:timer]) + %{state | timer: nil} + else + state + end + + ref = Process.monitor(client_pid) + + state = put_in(state.client_monitors[client_pid], ref) + {:noreply, state, :hibernate} + end + + @impl true + def handle_cast({:remove_client, client_pid}, %{key: key} = state) do + {{_conn_pid, used_by, _crf, _last_reference}, _} = + Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> + {conn_pid, List.delete(used_by, client_pid), crf, last_reference} + end) + + {ref, state} = pop_in(state.client_monitors[client_pid]) + Process.demonitor(ref) + + timer = + if used_by == [] do + max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000) + Process.send_after(self(), :idle_close, max_idle) + else + nil + end + + {:noreply, %{state | timer: timer}, :hibernate} + end + + @impl true + def handle_info(:idle_close, state) do + # Gun monitors the owner process, and will close the connection automatically + # when it's terminated + {:stop, :normal, state} + end + + # Gracefully shutdown if the connection got closed without any streams left + @impl true + def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do + {:stop, :normal, state} + end + + # Otherwise, shutdown with an error + @impl true + def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do + {:stop, {:error, down_message}, state} + end + + @impl true + def handle_info({:DOWN, _ref, :process, pid, reason}, state) do + # Sometimes the client is dead before we demonitor it in :remove_client, so the message + # arrives anyway + + case state.client_monitors[pid] do + nil -> + {:noreply, state, :hibernate} + + _ref -> + :telemetry.execute( + [:pleroma, :connection_pool, :client_death], + %{client_pid: pid, reason: reason}, + %{key: state.key} + ) + + handle_cast({:remove_client, pid}, state) + end + end + + # LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478 + defp crf(time_delta, prev_crf) do + 1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf + end +end diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex new file mode 100644 index 000000000..39615c956 --- /dev/null +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -0,0 +1,45 @@ +defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do + @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" + + use DynamicSupervisor + + def start_link(opts) do + DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__) + end + + def init(_opts) do + DynamicSupervisor.init( + strategy: :one_for_one, + max_children: Pleroma.Config.get([:connections_pool, :max_connections]) + ) + end + + def start_worker(opts, retry \\ false) do + case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do + {:error, :max_children} -> + if retry or free_pool() == :error do + :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts}) + {:error, :pool_full} + else + start_worker(opts, true) + end + + res -> + res + end + end + + defp free_pool do + wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor()) + end + + defp wait_for_reclaimer_finish({pid, mon}) do + receive do + {:DOWN, ^mon, :process, ^pid, :no_unused_conns} -> + :error + + {:DOWN, ^mon, :process, ^pid, :normal} -> + :ok + end + end +end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index d78c5f202..dc1b9b840 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -109,7 +109,7 @@ def extract_first_external_url(object, content) do result = content |> Floki.parse_fragment!() - |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]") + |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]") |> Floki.attribute("a", "href") |> Enum.at(0) diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex index 510722ff9..9ec3836b0 100644 --- a/lib/pleroma/http/adapter_helper.ex +++ b/lib/pleroma/http/adapter_helper.ex @@ -3,32 +3,30 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.AdapterHelper do - alias Pleroma.HTTP.Connection + @moduledoc """ + Configure Tesla.Client with default and customized adapter options. + """ + @defaults [pool: :federation] + + @type proxy_type() :: :socks4 | :socks5 + @type host() :: charlist() | :inet.ip_address() + + alias Pleroma.Config + alias Pleroma.HTTP.AdapterHelper + require Logger @type proxy :: {Connection.host(), pos_integer()} | {Connection.proxy_type(), Connection.host(), pos_integer()} @callback options(keyword(), URI.t()) :: keyword() - @callback after_request(keyword()) :: :ok - - @spec options(keyword(), URI.t()) :: keyword() - def options(opts, _uri) do - proxy = Pleroma.Config.get([:http, :proxy_url], nil) - maybe_add_proxy(opts, format_proxy(proxy)) - end - - @spec maybe_get_conn(URI.t(), keyword()) :: keyword() - def maybe_get_conn(_uri, opts), do: opts - - @spec after_request(keyword()) :: :ok - def after_request(_opts), do: :ok + @callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()} @spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil def format_proxy(nil), do: nil def format_proxy(proxy_url) do - case Connection.parse_proxy(proxy_url) do + case parse_proxy(proxy_url) do {:ok, host, port} -> {host, port} {:ok, type, host, port} -> {type, host, port} _ -> nil @@ -38,4 +36,105 @@ def format_proxy(proxy_url) do @spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword() def maybe_add_proxy(opts, nil), do: opts def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy) + + @doc """ + Merge default connection & adapter options with received ones. + """ + + @spec options(URI.t(), keyword()) :: keyword() + def options(%URI{} = uri, opts \\ []) do + @defaults + |> put_timeout() + |> Keyword.merge(opts) + |> adapter_helper().options(uri) + end + + # For Hackney, this is the time a connection can stay idle in the pool. + # For Gun, this is the timeout to receive a message from Gun. + defp put_timeout(opts) do + {config_key, default} = + if adapter() == Tesla.Adapter.Gun do + {:pools, Config.get([:pools, :default, :timeout], 5_000)} + else + {:hackney_pools, 10_000} + end + + timeout = Config.get([config_key, opts[:pool], :timeout], default) + + Keyword.merge(opts, timeout: timeout) + end + + def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts) + defp adapter, do: Application.get_env(:tesla, :adapter) + + defp adapter_helper do + case adapter() do + Tesla.Adapter.Gun -> AdapterHelper.Gun + Tesla.Adapter.Hackney -> AdapterHelper.Hackney + _ -> AdapterHelper.Default + end + end + + @spec parse_proxy(String.t() | tuple() | nil) :: + {:ok, host(), pos_integer()} + | {:ok, proxy_type(), host(), pos_integer()} + | {:error, atom()} + | nil + + def parse_proxy(nil), do: nil + + def parse_proxy(proxy) when is_binary(proxy) do + with [host, port] <- String.split(proxy, ":"), + {port, ""} <- Integer.parse(port) do + {:ok, parse_host(host), port} + else + {_, _} -> + Logger.warn("Parsing port failed #{inspect(proxy)}") + {:error, :invalid_proxy_port} + + :error -> + Logger.warn("Parsing port failed #{inspect(proxy)}") + {:error, :invalid_proxy_port} + + _ -> + Logger.warn("Parsing proxy failed #{inspect(proxy)}") + {:error, :invalid_proxy} + end + end + + def parse_proxy(proxy) when is_tuple(proxy) do + with {type, host, port} <- proxy do + {:ok, type, parse_host(host), port} + else + _ -> + Logger.warn("Parsing proxy failed #{inspect(proxy)}") + {:error, :invalid_proxy} + end + end + + @spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address() + def parse_host(host) when is_list(host), do: host + def parse_host(host) when is_atom(host), do: to_charlist(host) + + def parse_host(host) when is_binary(host) do + host = to_charlist(host) + + case :inet.parse_address(host) do + {:error, :einval} -> host + {:ok, ip} -> ip + end + end + + @spec format_host(String.t()) :: charlist() + def format_host(host) do + host_charlist = to_charlist(host) + + case :inet.parse_address(host_charlist) do + {:error, :einval} -> + :idna.encode(host_charlist) + + {:ok, _ip} -> + host_charlist + end + end end diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex new file mode 100644 index 000000000..e13441316 --- /dev/null +++ b/lib/pleroma/http/adapter_helper/default.ex @@ -0,0 +1,14 @@ +defmodule Pleroma.HTTP.AdapterHelper.Default do + alias Pleroma.HTTP.AdapterHelper + + @behaviour Pleroma.HTTP.AdapterHelper + + @spec options(keyword(), URI.t()) :: keyword() + def options(opts, _uri) do + proxy = Pleroma.Config.get([:http, :proxy_url], nil) + AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy)) + end + + @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} + def get_conn(_uri, opts), do: {:ok, opts} +end diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index ead7cdc6b..b4ff8306c 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -5,8 +5,8 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do @behaviour Pleroma.HTTP.AdapterHelper + alias Pleroma.Gun.ConnectionPool alias Pleroma.HTTP.AdapterHelper - alias Pleroma.Pool.Connections require Logger @@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do connect_timeout: 5_000, domain_lookup_timeout: 5_000, tls_handshake_timeout: 5_000, - retry: 1, + retry: 0, retry_timeout: 1000, await_up_timeout: 5_000 ] @@ -31,16 +31,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do |> Keyword.merge(config_opts) |> add_scheme_opts(uri) |> AdapterHelper.maybe_add_proxy(proxy) - |> maybe_get_conn(uri, incoming_opts) - end - - @spec after_request(keyword()) :: :ok - def after_request(opts) do - if opts[:conn] && opts[:body_as] != :chunks do - Connections.checkout(opts[:conn], self(), :gun_connections) - end - - :ok + |> Keyword.merge(incoming_opts) end defp add_scheme_opts(opts, %{scheme: "http"}), do: opts @@ -48,30 +39,40 @@ defp add_scheme_opts(opts, %{scheme: "http"}), do: opts defp add_scheme_opts(opts, %{scheme: "https"}) do opts |> Keyword.put(:certificates_verification, true) - |> Keyword.put(:tls_opts, log_level: :warning) end - defp maybe_get_conn(adapter_opts, uri, incoming_opts) do - {receive_conn?, opts} = - adapter_opts - |> Keyword.merge(incoming_opts) - |> Keyword.pop(:receive_conn, true) - - if Connections.alive?(:gun_connections) and receive_conn? do - checkin_conn(uri, opts) - else - opts + @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()} + def get_conn(uri, opts) do + case ConnectionPool.get_conn(uri, opts) do + {:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)} + err -> err end end - defp checkin_conn(uri, opts) do - case Connections.checkin(uri, :gun_connections) do - nil -> - Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts]) - opts + @prefix Pleroma.Gun.ConnectionPool + def limiter_setup do + wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait]) + retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries]) - conn when is_pid(conn) -> - Keyword.merge(opts, conn: conn, close_conn: false) - end + :pools + |> Pleroma.Config.get([]) + |> Enum.each(fn {name, opts} -> + max_running = Keyword.get(opts, :size, 50) + max_waiting = Keyword.get(opts, :max_waiting, 10) + + result = + ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting, + wait: wait, + max_retries: retries + ) + + case result do + :ok -> :ok + {:error, :existing} -> :ok + e -> raise e + end + end) + + :ok end end diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex index 3972a03a9..cd569422b 100644 --- a/lib/pleroma/http/adapter_helper/hackney.ex +++ b/lib/pleroma/http/adapter_helper/hackney.ex @@ -24,5 +24,6 @@ def options(connection_opts \\ [], %URI{} = uri) do defp add_scheme_opts(opts, _), do: opts - def after_request(_), do: :ok + @spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} + def get_conn(_uri, opts), do: {:ok, opts} end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex deleted file mode 100644 index ebacf7902..000000000 --- a/lib/pleroma/http/connection.ex +++ /dev/null @@ -1,124 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.Connection do - @moduledoc """ - Configure Tesla.Client with default and customized adapter options. - """ - - alias Pleroma.Config - alias Pleroma.HTTP.AdapterHelper - - require Logger - - @defaults [pool: :federation] - - @type ip_address :: ipv4_address() | ipv6_address() - @type ipv4_address :: {0..255, 0..255, 0..255, 0..255} - @type ipv6_address :: - {0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535} - @type proxy_type() :: :socks4 | :socks5 - @type host() :: charlist() | ip_address() - - @doc """ - Merge default connection & adapter options with received ones. - """ - - @spec options(URI.t(), keyword()) :: keyword() - def options(%URI{} = uri, opts \\ []) do - @defaults - |> pool_timeout() - |> Keyword.merge(opts) - |> adapter_helper().options(uri) - end - - defp pool_timeout(opts) do - {config_key, default} = - if adapter() == Tesla.Adapter.Gun do - {:pools, Config.get([:pools, :default, :timeout])} - else - {:hackney_pools, 10_000} - end - - timeout = Config.get([config_key, opts[:pool], :timeout], default) - - Keyword.merge(opts, timeout: timeout) - end - - @spec after_request(keyword()) :: :ok - def after_request(opts), do: adapter_helper().after_request(opts) - - defp adapter, do: Application.get_env(:tesla, :adapter) - - defp adapter_helper do - case adapter() do - Tesla.Adapter.Gun -> AdapterHelper.Gun - Tesla.Adapter.Hackney -> AdapterHelper.Hackney - _ -> AdapterHelper - end - end - - @spec parse_proxy(String.t() | tuple() | nil) :: - {:ok, host(), pos_integer()} - | {:ok, proxy_type(), host(), pos_integer()} - | {:error, atom()} - | nil - - def parse_proxy(nil), do: nil - - def parse_proxy(proxy) when is_binary(proxy) do - with [host, port] <- String.split(proxy, ":"), - {port, ""} <- Integer.parse(port) do - {:ok, parse_host(host), port} - else - {_, _} -> - Logger.warn("Parsing port failed #{inspect(proxy)}") - {:error, :invalid_proxy_port} - - :error -> - Logger.warn("Parsing port failed #{inspect(proxy)}") - {:error, :invalid_proxy_port} - - _ -> - Logger.warn("Parsing proxy failed #{inspect(proxy)}") - {:error, :invalid_proxy} - end - end - - def parse_proxy(proxy) when is_tuple(proxy) do - with {type, host, port} <- proxy do - {:ok, type, parse_host(host), port} - else - _ -> - Logger.warn("Parsing proxy failed #{inspect(proxy)}") - {:error, :invalid_proxy} - end - end - - @spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address() - def parse_host(host) when is_list(host), do: host - def parse_host(host) when is_atom(host), do: to_charlist(host) - - def parse_host(host) when is_binary(host) do - host = to_charlist(host) - - case :inet.parse_address(host) do - {:error, :einval} -> host - {:ok, ip} -> ip - end - end - - @spec format_host(String.t()) :: charlist() - def format_host(host) do - host_charlist = to_charlist(host) - - case :inet.parse_address(host_charlist) do - {:error, :einval} -> - :idna.encode(host_charlist) - - {:ok, _ip} -> - host_charlist - end - end -end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index 66ca75367..6128bc4cf 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -7,7 +7,7 @@ defmodule Pleroma.HTTP do Wrapper for `Tesla.request/2`. """ - alias Pleroma.HTTP.Connection + alias Pleroma.HTTP.AdapterHelper alias Pleroma.HTTP.Request alias Pleroma.HTTP.RequestBuilder, as: Builder alias Tesla.Client @@ -60,49 +60,29 @@ def post(url, body, headers \\ [], options \\ []), {:ok, Env.t()} | {:error, any()} def request(method, url, body, headers, options) when is_binary(url) do uri = URI.parse(url) - adapter_opts = Connection.options(uri, options[:adapter] || []) - options = put_in(options[:adapter], adapter_opts) - params = options[:params] || [] - request = build_request(method, headers, options, url, body, params) + adapter_opts = AdapterHelper.options(uri, options[:adapter] || []) - adapter = Application.get_env(:tesla, :adapter) - client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter) + case AdapterHelper.get_conn(uri, adapter_opts) do + {:ok, adapter_opts} -> + options = put_in(options[:adapter], adapter_opts) + params = options[:params] || [] + request = build_request(method, headers, options, url, body, params) - pid = Process.whereis(adapter_opts[:pool]) + adapter = Application.get_env(:tesla, :adapter) + client = Tesla.client([Pleroma.HTTP.Middleware.FollowRedirects], adapter) - pool_alive? = - if adapter == Tesla.Adapter.Gun && pid do - Process.alive?(pid) - else - false - end + maybe_limit( + fn -> + request(client, request) + end, + adapter, + adapter_opts + ) - request_opts = - adapter_opts - |> Enum.into(%{}) - |> Map.put(:env, Pleroma.Config.get([:env])) - |> Map.put(:pool_alive?, pool_alive?) - - response = request(client, request, request_opts) - - Connection.after_request(adapter_opts) - - response - end - - @spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()} - def request(%Client{} = client, request, %{env: :test}), do: request(client, request) - - def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request) - - def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request) - - def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do - :poolboy.transaction( - pool, - &Pleroma.Pool.Request.execute(&1, client, request, timeout), - timeout - ) + # Connection release is handled in a custom FollowRedirects middleware + err -> + err + end end @spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()} @@ -118,4 +98,13 @@ defp build_request(method, headers, options, url, body, params) do |> Builder.add_param(:query, :query, params) |> Builder.convert_to_keyword() end + + @prefix Pleroma.Gun.ConnectionPool + defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do + ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun) + end + + defp maybe_limit(fun, _, _) do + fun.() + end end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 74458c09a..a1f935232 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do schema "instances" do field(:host, :string) field(:unreachable_since, :naive_datetime_usec) + field(:favicon, :string) + field(:favicon_updated_at, :naive_datetime) timestamps() end @@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do def changeset(struct, params \\ %{}) do struct - |> cast(params, [:host, :unreachable_since]) + |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at]) |> validate_required([:host]) |> unique_constraint(:host) end @@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do end defp parse_datetime(datetime), do: datetime + + def get_or_update_favicon(%URI{host: host} = instance_uri) do + existing_record = Repo.get_by(Instance, %{host: host}) + now = NaiveDateTime.utc_now() + + if existing_record && existing_record.favicon_updated_at && + NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do + existing_record.favicon + else + favicon = scrape_favicon(instance_uri) + + if existing_record do + existing_record + |> changeset(%{favicon: favicon, favicon_updated_at: now}) + |> Repo.update() + else + %Instance{} + |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now}) + |> Repo.insert() + end + + favicon + end + end + + defp scrape_favicon(%URI{} = instance_uri) do + try do + with {:ok, %Tesla.Env{body: html}} <- + Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), + favicon_rel <- + html + |> Floki.parse_document!() + |> Floki.attribute("link[rel=icon]", "href") + |> List.first(), + favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), + true <- is_binary(favicon) do + favicon + else + _ -> nil + end + rescue + _ -> nil + end + end end diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index b3770307a..d260e62ca 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MigrationHelper.NotificationBackfill do - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -25,18 +24,27 @@ def fill_in_notification_types do |> type_from_activity() notification - |> Notification.changeset(%{type: type}) + |> Ecto.Changeset.change(%{type: type}) |> Repo.update() end) end + defp get_by_ap_id(ap_id) do + q = + from(u in User, + select: u.id + ) + + Repo.get_by(q, ap_id: ap_id) + end + # This is copied over from Notifications to keep this stable. defp type_from_activity(%{data: %{"type" => type}} = activity) do case type do "Follow" -> accepted_function = fn activity -> - with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), - %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do + with %User{} = follower <- get_by_ap_id(activity.data["actor"]), + %User{} = followed <- get_by_ap_id(activity.data["object"]) do Pleroma.FollowingRelationship.following?(follower, followed) end end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 9ee9606be..0b171563b 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do |> preload([n, a, o], activity: {a, object: o}) |> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_blocked(user, exclude_blocked_opts) + |> exclude_filtered(user) |> exclude_visibility(opts) end @@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do |> where([n, a, o, tm], is_nil(tm.user_id)) end + defp exclude_filtered(query, user) do + case Pleroma.Filter.compose_regex(user) do + nil -> + query + + regex -> + from([_n, a, o] in query, + where: + fragment("not(?->>'content' ~* ?)", o.data, ^regex) or + fragment("?->>'actor' = ?", o.data, ^user.ap_id) + ) + end + end + @valid_visibilities ~w[direct unlisted public private] defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -337,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do end end + @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []} def create_notifications(activity, options \\ []) def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do @@ -367,6 +383,7 @@ defp do_create_notifications(%Activity{} = activity, options) do do_send = do_send && user in enabled_receivers create_notification(activity, user, do_send) end) + |> Enum.reject(&is_nil/1) {:ok, notifications} end @@ -480,6 +497,10 @@ def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_i end end + def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do + [object_id] + end + def get_potential_receiver_ap_ids(activity) do [] |> Utils.maybe_notify_to_recipients(activity) @@ -550,11 +571,9 @@ def skip?(%Activity{} = activity, %User{} = user) do [ :self, :invisible, - :followers, - :follows, - :non_followers, - :non_follows, - :recently_followed + :block_from_strangers, + :recently_followed, + :filtered ] |> Enum.find(&skip?(&1, activity, user)) end @@ -573,45 +592,15 @@ def skip?(:invisible, %Activity{} = activity, _) do end def skip?( - :followers, + :block_from_strangers, %Activity{} = activity, - %User{notification_settings: %{followers: false}} = user - ) do - actor = activity.data["actor"] - follower = User.get_cached_by_ap_id(actor) - User.following?(follower, user) - end - - def skip?( - :non_followers, - %Activity{} = activity, - %User{notification_settings: %{non_followers: false}} = user + %User{notification_settings: %{block_from_strangers: true}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) !User.following?(follower, user) end - def skip?( - :follows, - %Activity{} = activity, - %User{notification_settings: %{follows: false}} = user - ) do - actor = activity.data["actor"] - followed = User.get_cached_by_ap_id(actor) - User.following?(user, followed) - end - - def skip?( - :non_follows, - %Activity{} = activity, - %User{notification_settings: %{non_follows: false}} = user - ) do - actor = activity.data["actor"] - followed = User.get_cached_by_ap_id(actor) - !User.following?(user, followed) - end - # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do actor = activity.data["actor"] @@ -623,6 +612,26 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, end) end + def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false + + def skip?(:filtered, activity, user) do + object = Object.normalize(activity) + + cond do + is_nil(object) -> + false + + object.data["actor"] == user.ap_id -> + false + + not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> + Regex.match?(regex, object.data["content"]) + + true -> + false + end + end + def skip?(_, _, _), do: false def for_user_and_activity(user, activity) do diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 263ded5dd..3e2949ee2 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -83,8 +83,8 @@ def fetch_object_from_id(id, options \\ []) do {:transmogrifier, {:error, {:reject, nil}}} -> {:reject, nil} - {:transmogrifier, _} -> - {:error, "Transmogrifier failure."} + {:transmogrifier, _} = e -> + {:error, e} {:object, data, nil} -> reinject_object(%Object{}, data) diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex index b4b47a31f..2e54df47a 100644 --- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex +++ b/lib/pleroma/plugs/admin_secret_authentication_plug.ex @@ -4,6 +4,9 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do import Plug.Conn + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.RateLimiter alias Pleroma.User def init(options) do @@ -11,7 +14,10 @@ def init(options) do end def secret_token do - Pleroma.Config.get(:admin_token) + case Pleroma.Config.get(:admin_token) do + blank when blank in [nil, ""] -> nil + token -> token + end end def call(%{assigns: %{user: %User{}}} = conn, _), do: conn @@ -26,9 +32,9 @@ def call(conn, _) do def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do if admin_token == secret_token() do - assign(conn, :user, %User{is_admin: true}) + assign_admin_user(conn) else - conn + handle_bad_token(conn) end end @@ -36,8 +42,19 @@ def authenticate(conn) do token = secret_token() case get_req_header(conn, "x-admin-token") do - [^token] -> assign(conn, :user, %User{is_admin: true}) - _ -> conn + blank when blank in [[], [""]] -> conn + [^token] -> assign_admin_user(conn) + _ -> handle_bad_token(conn) end end + + defp assign_admin_user(conn) do + conn + |> assign(:user, %User{is_admin: true}) + |> OAuthScopesPlug.skip_plug() + end + + defp handle_bad_token(conn) do + RateLimiter.call(conn, name: :authentication) + end end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 1420a9611..c363b193b 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -69,10 +69,11 @@ defp csp_string do img_src = "img-src 'self' data: blob:" media_src = "media-src 'self'" + # Strict multimedia CSP enforcement only when MediaProxy is enabled {img_src, media_src} = if Config.get([:media_proxy, :enabled]) && !Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do - sources = get_proxy_and_attachment_sources() + sources = build_csp_multimedia_source_list() {[img_src, sources], [media_src, sources]} else {[img_src, " https:"], [media_src, " https:"]} @@ -81,14 +82,14 @@ defp csp_string do connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url] connect_src = - if Pleroma.Config.get(:env) == :dev do + if Config.get(:env) == :dev do [connect_src, " http://localhost:3035/"] else connect_src end script_src = - if Pleroma.Config.get(:env) == :dev do + if Config.get(:env) == :dev do "script-src 'self' 'unsafe-eval'" else "script-src 'self'" @@ -107,38 +108,64 @@ defp csp_string do |> :erlang.iolist_to_binary() end - defp get_proxy_and_attachment_sources do + defp build_csp_from_whitelist([], acc), do: acc + + defp build_csp_from_whitelist([last], acc) do + [build_csp_param_from_whitelist(last) | acc] + end + + defp build_csp_from_whitelist([head | tail], acc) do + build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc]) + end + + # TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist + defp build_csp_param_from_whitelist("http" <> _ = url) do + build_csp_param(url) + end + + defp build_csp_param_from_whitelist(url), do: url + + defp build_csp_multimedia_source_list do media_proxy_whitelist = - Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> - add_source(acc, host) - end) + [:media_proxy, :whitelist] + |> Config.get() + |> build_csp_from_whitelist([]) - media_proxy_base_url = - if Config.get([:media_proxy, :base_url]), - do: URI.parse(Config.get([:media_proxy, :base_url])).host + captcha_method = Config.get([Pleroma.Captcha, :method]) + captcha_endpoint = Config.get([captcha_method, :endpoint]) - upload_base_url = - if Config.get([Pleroma.Upload, :base_url]), - do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host + base_endpoints = + [ + [:media_proxy, :base_url], + [Pleroma.Upload, :base_url], + [Pleroma.Uploaders.S3, :public_endpoint] + ] + |> Enum.map(&Config.get/1) - s3_endpoint = - if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3, - do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host - - [] - |> add_source(media_proxy_base_url) - |> add_source(upload_base_url) - |> add_source(s3_endpoint) + [captcha_endpoint | base_endpoints] + |> Enum.map(&build_csp_param/1) + |> Enum.reduce([], &add_source(&2, &1)) |> add_source(media_proxy_whitelist) end defp add_source(iodata, nil), do: iodata + defp add_source(iodata, []), do: iodata defp add_source(iodata, source), do: [[?\s, source] | iodata] defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] + defp build_csp_param(nil), do: nil + + defp build_csp_param(url) when is_binary(url) do + %{host: host, scheme: scheme} = URI.parse(url) + + if scheme do + [scheme, "://", host] + end + end + def warn_if_disabled do unless Config.get([:http_security, :enabled]) do Logger.warn(" diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index 156e6788e..143665c71 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do def init(options), do: options def call(conn, _) do - if enabled?() and accepts_html?(conn) do + if enabled?() and requires_html?(conn) do conn |> StaticFEController.call(:show) |> halt() @@ -20,10 +20,7 @@ def call(conn, _) do defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) - defp accepts_html?(conn) do - case get_req_header(conn, "accept") do - [accept | _] -> String.contains?(accept, "text/html") - _ -> false - end + defp requires_html?(conn) do + Phoenix.Controller.get_format(conn) == "html" end end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 2748102df..488a61d1d 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do import Plug.Conn alias Pleroma.User - alias Pleroma.Web.OAuth def init(options) do options end - def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do - token = assigns[:token] - - cond do - not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> - conn - - token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> - # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. - # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. - # Admin might opt out of admin scope for some apps to block any admin actions from them. - conn - - true -> - fail(conn) - end + def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do + conn end def call(conn, _) do - fail(conn) - end - - defp fail(conn) do conn - |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") + |> render_error(:forbidden, "User is not an admin.") |> halt() end end diff --git a/lib/pleroma/pool/connections.ex b/lib/pleroma/pool/connections.ex deleted file mode 100644 index acafe1bea..000000000 --- a/lib/pleroma/pool/connections.ex +++ /dev/null @@ -1,283 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Connections do - use GenServer - - alias Pleroma.Config - alias Pleroma.Gun - - require Logger - - @type domain :: String.t() - @type conn :: Pleroma.Gun.Conn.t() - - @type t :: %__MODULE__{ - conns: %{domain() => conn()}, - opts: keyword() - } - - defstruct conns: %{}, opts: [] - - @spec start_link({atom(), keyword()}) :: {:ok, pid()} - def start_link({name, opts}) do - GenServer.start_link(__MODULE__, opts, name: name) - end - - @impl true - def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}} - - @spec checkin(String.t() | URI.t(), atom()) :: pid() | nil - def checkin(url, name) - def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name) - - def checkin(%URI{} = uri, name) do - timeout = Config.get([:connections_pool, :checkin_timeout], 250) - - GenServer.call(name, {:checkin, uri}, timeout) - end - - @spec alive?(atom()) :: boolean() - def alive?(name) do - if pid = Process.whereis(name) do - Process.alive?(pid) - else - false - end - end - - @spec get_state(atom()) :: t() - def get_state(name) do - GenServer.call(name, :state) - end - - @spec count(atom()) :: pos_integer() - def count(name) do - GenServer.call(name, :count) - end - - @spec get_unused_conns(atom()) :: [{domain(), conn()}] - def get_unused_conns(name) do - GenServer.call(name, :unused_conns) - end - - @spec checkout(pid(), pid(), atom()) :: :ok - def checkout(conn, pid, name) do - GenServer.cast(name, {:checkout, conn, pid}) - end - - @spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok - def add_conn(name, key, conn) do - GenServer.cast(name, {:add_conn, key, conn}) - end - - @spec remove_conn(atom(), String.t()) :: :ok - def remove_conn(name, key) do - GenServer.cast(name, {:remove_conn, key}) - end - - @impl true - def handle_cast({:add_conn, key, conn}, state) do - state = put_in(state.conns[key], conn) - - Process.monitor(conn.conn) - {:noreply, state} - end - - @impl true - def handle_cast({:checkout, conn_pid, pid}, state) do - state = - with true <- Process.alive?(conn_pid), - {key, conn} <- find_conn(state.conns, conn_pid), - used_by <- List.keydelete(conn.used_by, pid, 0) do - conn_state = if used_by == [], do: :idle, else: conn.conn_state - - put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by}) - else - false -> - Logger.debug("checkout for closed conn #{inspect(conn_pid)}") - state - - nil -> - Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state") - state - end - - {:noreply, state} - end - - @impl true - def handle_cast({:remove_conn, key}, state) do - state = put_in(state.conns, Map.delete(state.conns, key)) - {:noreply, state} - end - - @impl true - def handle_call({:checkin, uri}, from, state) do - key = "#{uri.scheme}:#{uri.host}:#{uri.port}" - - case state.conns[key] do - %{conn: pid, gun_state: :up} = conn -> - time = :os.system_time(:second) - last_reference = time - conn.last_reference - crf = crf(last_reference, 100, conn.crf) - - state = - put_in(state.conns[key], %{ - conn - | last_reference: time, - crf: crf, - conn_state: :active, - used_by: [from | conn.used_by] - }) - - {:reply, pid, state} - - %{gun_state: :down} -> - {:reply, nil, state} - - nil -> - {:reply, nil, state} - end - end - - @impl true - def handle_call(:state, _from, state), do: {:reply, state, state} - - @impl true - def handle_call(:count, _from, state) do - {:reply, Enum.count(state.conns), state} - end - - @impl true - def handle_call(:unused_conns, _from, state) do - unused_conns = - state.conns - |> Enum.filter(&filter_conns/1) - |> Enum.sort(&sort_conns/2) - - {:reply, unused_conns, state} - end - - defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true - defp filter_conns(_), do: false - - defp sort_conns({_, c1}, {_, c2}) do - c1.crf <= c2.crf and c1.last_reference <= c2.last_reference - end - - @impl true - def handle_info({:gun_up, conn_pid, _protocol}, state) do - %{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid) - - host = - case :inet.ntoa(host) do - {:error, :einval} -> host - ip -> ip - end - - key = "#{scheme}:#{host}:#{port}" - - state = - with {key, conn} <- find_conn(state.conns, conn_pid, key), - {true, key} <- {Process.alive?(conn_pid), key} do - put_in(state.conns[key], %{ - conn - | gun_state: :up, - conn_state: :active, - retries: 0 - }) - else - {false, key} -> - put_in( - state.conns, - Map.delete(state.conns, key) - ) - - nil -> - :ok = Gun.close(conn_pid) - - state - end - - {:noreply, state} - end - - @impl true - def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do - retries = Config.get([:connections_pool, :retry], 1) - # we can't get info on this pid, because pid is dead - state = - with {key, conn} <- find_conn(state.conns, conn_pid), - {true, key} <- {Process.alive?(conn_pid), key} do - if conn.retries == retries do - :ok = Gun.close(conn.conn) - - put_in( - state.conns, - Map.delete(state.conns, key) - ) - else - put_in(state.conns[key], %{ - conn - | gun_state: :down, - retries: conn.retries + 1 - }) - end - else - {false, key} -> - put_in( - state.conns, - Map.delete(state.conns, key) - ) - - nil -> - Logger.debug(":gun_down for conn which isn't found in state") - - state - end - - {:noreply, state} - end - - @impl true - def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do - Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") - - state = - with {key, conn} <- find_conn(state.conns, conn_pid) do - Enum.each(conn.used_by, fn {pid, _ref} -> - Process.exit(pid, reason) - end) - - put_in( - state.conns, - Map.delete(state.conns, key) - ) - else - nil -> - Logger.debug(":DOWN for conn which isn't found in state") - - state - end - - {:noreply, state} - end - - defp find_conn(conns, conn_pid) do - Enum.find(conns, fn {_key, conn} -> - conn.conn == conn_pid - end) - end - - defp find_conn(conns, conn_pid, conn_key) do - Enum.find(conns, fn {key, conn} -> - key == conn_key and conn.conn == conn_pid - end) - end - - def crf(current, steps, crf) do - 1 + :math.pow(0.5, current / steps) * crf - end -end diff --git a/lib/pleroma/pool/pool.ex b/lib/pleroma/pool/pool.ex deleted file mode 100644 index 21a6fbbc5..000000000 --- a/lib/pleroma/pool/pool.ex +++ /dev/null @@ -1,22 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool do - def child_spec(opts) do - poolboy_opts = - opts - |> Keyword.put(:worker_module, Pleroma.Pool.Request) - |> Keyword.put(:name, {:local, opts[:name]}) - |> Keyword.put(:size, opts[:size]) - |> Keyword.put(:max_overflow, opts[:max_overflow]) - - %{ - id: opts[:id] || {__MODULE__, make_ref()}, - start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]}, - restart: :permanent, - shutdown: 5000, - type: :worker - } - end -end diff --git a/lib/pleroma/pool/request.ex b/lib/pleroma/pool/request.ex deleted file mode 100644 index 3fb930db7..000000000 --- a/lib/pleroma/pool/request.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Request do - use GenServer - - require Logger - - def start_link(args) do - GenServer.start_link(__MODULE__, args) - end - - @impl true - def init(_), do: {:ok, []} - - @spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) :: - {:ok, Tesla.Env.t()} | {:error, any()} - def execute(pid, client, request, timeout) do - GenServer.call(pid, {:execute, client, request}, timeout) - end - - @impl true - def handle_call({:execute, client, request}, _from, state) do - response = Pleroma.HTTP.request(client, request) - - {:reply, response, state} - end - - @impl true - def handle_info({:gun_data, _conn, _stream, _, _}, state) do - {:noreply, state} - end - - @impl true - def handle_info({:gun_up, _conn, _protocol}, state) do - {:noreply, state} - end - - @impl true - def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do - {:noreply, state} - end - - @impl true - def handle_info({:gun_error, _conn, _stream, _error}, state) do - {:noreply, state} - end - - @impl true - def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do - {:noreply, state} - end - - @impl true - def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do - {:noreply, state} - end - - @impl true - def handle_info(msg, state) do - Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}") - {:noreply, state} - end -end diff --git a/lib/pleroma/pool/supervisor.ex b/lib/pleroma/pool/supervisor.ex deleted file mode 100644 index faf646cb2..000000000 --- a/lib/pleroma/pool/supervisor.ex +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.Supervisor do - use Supervisor - - alias Pleroma.Config - alias Pleroma.Pool - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, name: __MODULE__) - end - - def init(_) do - conns_child = %{ - id: Pool.Connections, - start: - {Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]} - } - - Supervisor.init([conns_child | pools()], strategy: :one_for_one) - end - - defp pools do - pools = Config.get(:pools) - - pools = - if Config.get([Pleroma.Upload, :proxy_remote]) == false do - Keyword.delete(pools, :upload) - else - pools - end - - for {pool_name, pool_opts} <- pools do - pool_opts - |> Keyword.put(:id, {Pool, pool_name}) - |> Keyword.put(:name, pool_name) - |> Pool.child_spec() - end - end -end diff --git a/lib/pleroma/reverse_proxy/client/tesla.ex b/lib/pleroma/reverse_proxy/client/tesla.ex index e81ea8bde..65785445d 100644 --- a/lib/pleroma/reverse_proxy/client/tesla.ex +++ b/lib/pleroma/reverse_proxy/client/tesla.ex @@ -48,7 +48,7 @@ def stream_body(%{pid: pid, opts: opts, fin: true}) do # if there were redirects we need to checkout old conn conn = opts[:old_conn] || opts[:conn] - if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections) + if conn, do: :ok = Pleroma.Gun.ConnectionPool.release_conn(conn) :done end diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 4bbeb493c..28ad4c846 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -3,12 +3,13 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy do + @range_headers ~w(range if-range) @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ - ~w(if-unmodified-since if-none-match if-range range) + ~w(if-unmodified-since if-none-match) ++ @range_headers @resp_cache_headers ~w(etag date last-modified) @keep_resp_headers @resp_cache_headers ++ - ~w(content-type content-disposition content-encoding content-range) ++ - ~w(accept-ranges vary) + ~w(content-length content-type content-disposition content-encoding) ++ + ~w(content-range accept-ranges vary) @default_cache_control_header "public, max-age=1209600" @valid_resp_codes [200, 206, 304] @max_read_duration :timer.seconds(30) @@ -170,6 +171,8 @@ defp request(method, url, headers, opts) do end defp response(conn, client, url, status, headers, opts) do + Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}") + result = conn |> put_resp_headers(build_resp_headers(headers, opts)) @@ -220,7 +223,9 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do end end - defp head_response(conn, _url, code, headers, opts) do + defp head_response(conn, url, code, headers, opts) do + Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}") + conn |> put_resp_headers(build_resp_headers(headers, opts)) |> send_resp(code, "") @@ -262,20 +267,33 @@ defp build_req_headers(headers, opts) do headers |> downcase_headers() |> Enum.filter(fn {k, _} -> k in @keep_req_headers end) - |> (fn headers -> - headers = headers ++ Keyword.get(opts, :req_headers, []) + |> build_req_range_or_encoding_header(opts) + |> build_req_user_agent_header(opts) + |> Keyword.merge(Keyword.get(opts, :req_headers, [])) + end - if Keyword.get(opts, :keep_user_agent, false) do - List.keystore( - headers, - "user-agent", - 0, - {"user-agent", Pleroma.Application.user_agent()} - ) - else - headers - end - end).() + # Disable content-encoding if any @range_headers are requested (see #1823). + defp build_req_range_or_encoding_header(headers, _opts) do + range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end) + + if range? && List.keymember?(headers, "accept-encoding", 0) do + List.keydelete(headers, "accept-encoding", 0) + else + headers + end + end + + defp build_req_user_agent_header(headers, opts) do + if Keyword.get(opts, :keep_user_agent, false) do + List.keystore( + headers, + "user-agent", + 0, + {"user-agent", Pleroma.Application.user_agent()} + ) + else + headers + end end defp build_resp_headers(headers, opts) do @@ -283,7 +301,7 @@ defp build_resp_headers(headers, opts) do |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> build_resp_cache_headers(opts) |> build_resp_content_disposition_header(opts) - |> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() + |> Keyword.merge(Keyword.get(opts, :resp_headers, [])) end defp build_resp_cache_headers(headers, _opts) do diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex new file mode 100644 index 000000000..4cacae02f --- /dev/null +++ b/lib/pleroma/telemetry/logger.ex @@ -0,0 +1,76 @@ +defmodule Pleroma.Telemetry.Logger do + @moduledoc "Transforms Pleroma telemetry events to logs" + + require Logger + + @events [ + [:pleroma, :connection_pool, :reclaim, :start], + [:pleroma, :connection_pool, :reclaim, :stop], + [:pleroma, :connection_pool, :provision_failure], + [:pleroma, :connection_pool, :client_death] + ] + def attach do + :telemetry.attach_many("pleroma-logger", @events, &handle_event/4, []) + end + + # Passing anonymous functions instead of strings to logger is intentional, + # that way strings won't be concatenated if the message is going to be thrown + # out anyway due to higher log level configured + + def handle_event( + [:pleroma, :connection_pool, :reclaim, :start], + _, + %{max_connections: max_connections, reclaim_max: reclaim_max}, + _ + ) do + Logger.debug(fn -> + "Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{ + reclaim_max + } connections" + end) + end + + def handle_event( + [:pleroma, :connection_pool, :reclaim, :stop], + %{reclaimed_count: 0}, + _, + _ + ) do + Logger.error(fn -> + "Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts" + end) + end + + def handle_event( + [:pleroma, :connection_pool, :reclaim, :stop], + %{reclaimed_count: reclaimed_count}, + _, + _ + ) do + Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end) + end + + def handle_event( + [:pleroma, :connection_pool, :provision_failure], + %{opts: [key | _]}, + _, + _ + ) do + Logger.error(fn -> + "Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion" + end) + end + + def handle_event( + [:pleroma, :connection_pool, :client_death], + %{client_pid: client_pid, reason: reason}, + %{key: key}, + _ + ) do + Logger.warn(fn -> + "Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{ + inspect(reason) + }" + end) + end +end diff --git a/lib/pleroma/tesla/middleware/follow_redirects.ex b/lib/pleroma/tesla/middleware/follow_redirects.ex new file mode 100644 index 000000000..5a7032215 --- /dev/null +++ b/lib/pleroma/tesla/middleware/follow_redirects.ex @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2015-2020 Tymon Tobolski +# Copyright © 2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.Middleware.FollowRedirects do + @moduledoc """ + Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex + + Follow 3xx redirects + ## Options + - `:max_redirects` - limit number of redirects (default: `5`) + """ + + alias Pleroma.Gun.ConnectionPool + + @behaviour Tesla.Middleware + + @max_redirects 5 + @redirect_statuses [301, 302, 303, 307, 308] + + @impl Tesla.Middleware + def call(env, next, opts \\ []) do + max = Keyword.get(opts, :max_redirects, @max_redirects) + + redirect(env, next, max) + end + + defp redirect(env, next, left) do + opts = env.opts[:adapter] + + case Tesla.run(env, next) do + {:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 -> + release_conn(opts) + + case Tesla.get_header(res, "location") do + nil -> + {:ok, res} + + location -> + location = parse_location(location, res) + + case get_conn(location, opts) do + {:ok, opts} -> + %{env | opts: Keyword.put(env.opts, :adapter, opts)} + |> new_request(res.status, location) + |> redirect(next, left - 1) + + e -> + e + end + end + + {:ok, %{status: status}} when status in @redirect_statuses -> + release_conn(opts) + {:error, {__MODULE__, :too_many_redirects}} + + {:error, _} = e -> + release_conn(opts) + e + + other -> + unless opts[:body_as] == :chunks do + release_conn(opts) + end + + other + end + end + + defp get_conn(location, opts) do + uri = URI.parse(location) + + case ConnectionPool.get_conn(uri, opts) do + {:ok, conn} -> + {:ok, Keyword.merge(opts, conn: conn)} + + e -> + e + end + end + + defp release_conn(opts) do + ConnectionPool.release_conn(opts[:conn]) + end + + # The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally + # requested resource is not available, however a related resource (or another redirect) + # available via GET is available at the specified location. + # https://tools.ietf.org/html/rfc7231#section-6.4.4 + defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []} + + # The 307 (Temporary Redirect) status code indicates that the target + # resource resides temporarily under a different URI and the user agent + # MUST NOT change the request method (...) + # https://tools.ietf.org/html/rfc7231#section-6.4.7 + defp new_request(env, 307, location), do: %{env | url: location} + + defp new_request(env, _, location), do: %{env | url: location, query: []} + + defp parse_location("https://" <> _rest = location, _env), do: location + defp parse_location("http://" <> _rest = location, _env), do: location + + defp parse_location(location, env) do + env.url + |> URI.parse() + |> URI.merge(location) + |> URI.to_string() + end +end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..0fa6b89dc 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -63,6 +63,10 @@ def store(upload, opts \\ []) do with {:ok, upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), + description = Map.get(opts, :description) || upload.name, + {_, true} <- + {:description_limit, + String.length(description) <= Pleroma.Config.get([:instance, :description_limit])}, {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do {:ok, %{ @@ -75,9 +79,12 @@ def store(upload, opts \\ []) do "href" => url_from_spec(upload, opts.base_url, url_spec) } ], - "name" => Map.get(opts, :description) || upload.name + "name" => description }} else + {:description_limit, _} -> + {:error, :description_too_long} + {:error, error} -> Logger.error( "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool.ex new file mode 100644 index 000000000..c7fb6aefa --- /dev/null +++ b/lib/pleroma/upload/filter/exiftool.ex @@ -0,0 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.Exiftool do + @moduledoc """ + Strips GPS related EXIF tags and overwrites the file in place. + Also strips or replaces filesystem metadata e.g., timestamps. + """ + @behaviour Pleroma.Upload.Filter + + def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do + System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) + :ok + end + + def filter(_), do: :ok +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1d70a37ef..9240e912d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -89,7 +89,7 @@ defmodule Pleroma.User do field(:keys, :string) field(:public_key, :string) field(:ap_id, :string) - field(:avatar, :map) + field(:avatar, :map, default: %{}) field(:local, :boolean, default: true) field(:follower_address, :string) field(:following_address, :string) @@ -115,7 +115,7 @@ defmodule Pleroma.User do field(:is_moderator, :boolean, default: false) field(:is_admin, :boolean, default: false) field(:show_role, :boolean, default: true) - field(:settings, :map, default: nil) + field(:mastofe_settings, :map, default: nil) field(:uri, ObjectValidators.Uri, default: nil) field(:hide_followers_count, :boolean, default: false) field(:hide_follows_count, :boolean, default: false) @@ -138,6 +138,7 @@ defmodule Pleroma.User do field(:also_known_as, {:array, :string}, default: []) field(:inbox, :string) field(:shared_inbox, :string) + field(:accepts_chat_messages, :boolean, default: nil) embeds_one( :notification_settings, @@ -388,8 +389,8 @@ defp fix_follower_address(%{nickname: nickname} = params), defp fix_follower_address(params), do: params def remote_user_changeset(struct \\ %User{local: false}, params) do - bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) - name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + bio_limit = Config.get([:instance, :user_bio_length], 5000) + name_limit = Config.get([:instance, :user_name_length], 100) name = case params[:name] do @@ -436,7 +437,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :discoverable, :invisible, :actor_type, - :also_known_as + :also_known_as, + :accepts_chat_messages ] ) |> validate_required([:name, :ap_id]) @@ -448,8 +450,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do end def update_changeset(struct, params \\ %{}) do - bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) - name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + bio_limit = Config.get([:instance, :user_bio_length], 5000) + name_limit = Config.get([:instance, :user_name_length], 100) struct |> cast( @@ -481,7 +483,8 @@ def update_changeset(struct, params \\ %{}) do :pleroma_settings_store, :discoverable, :actor_type, - :also_known_as + :also_known_as, + :accepts_chat_messages ] ) |> unique_constraint(:nickname) @@ -527,11 +530,21 @@ defp parse_fields(value) do end defp put_emoji(changeset) do - bio = get_change(changeset, :bio) - name = get_change(changeset, :name) + emojified_fields = [:bio, :name, :raw_fields] + + if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do + bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio)) + name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name)) + + emoji = Map.merge(bio, name) + + emoji = + changeset + |> get_field(:raw_fields) + |> Enum.reduce(emoji, fn x, acc -> + Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"])) + end) - if bio || name do - emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name)) put_change(changeset, :emoji, emoji) else changeset @@ -539,14 +552,11 @@ defp put_emoji(changeset) do end defp put_change_if_present(changeset, map_field, value_function) do - if value = get_change(changeset, map_field) do - with {:ok, new_value} <- value_function.(value) do - put_change(changeset, map_field, new_value) - else - _ -> changeset - end + with {:ok, value} <- fetch_change(changeset, map_field), + {:ok, new_value} <- value_function.(value) do + put_change(changeset, map_field, new_value) else - changeset + _ -> changeset end end @@ -621,12 +631,13 @@ def force_password_reset_async(user) do def force_password_reset(user), do: update_password_reset_pending(user, true) def register_changeset(struct, params \\ %{}, opts \\ []) do - bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) - name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + bio_limit = Config.get([:instance, :user_bio_length], 5000) + name_limit = Config.get([:instance, :user_name_length], 100) + params = Map.put_new(params, :accepts_chat_messages, true) need_confirmation? = if is_nil(opts[:need_confirmation]) do - Pleroma.Config.get([:instance, :account_activation_required]) + Config.get([:instance, :account_activation_required]) else opts[:need_confirmation] end @@ -641,13 +652,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do :nickname, :password, :password_confirmation, - :emoji + :emoji, + :accepts_chat_messages ]) |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) |> unique_constraint(:email) |> unique_constraint(:nickname) - |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) + |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) |> validate_length(:bio, max: bio_limit) @@ -662,7 +674,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do def maybe_validate_required_email(changeset, true), do: changeset def maybe_validate_required_email(changeset, _) do - if Pleroma.Config.get([:instance, :account_activation_required]) do + if Config.get([:instance, :account_activation_required]) do validate_required(changeset, [:email]) else changeset @@ -682,7 +694,7 @@ defp put_following_and_follower_address(changeset) do end defp autofollow_users(user) do - candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) + candidates = Config.get([:instance, :autofollowed_nicknames]) autofollowed_users = User.Query.build(%{nickname: candidates, local: true, deactivated: false}) @@ -709,7 +721,7 @@ def post_register_action(%User{} = user) do def try_send_confirmation_email(%User{} = user) do if user.confirmation_pending && - Pleroma.Config.get([:instance, :account_activation_required]) do + Config.get([:instance, :account_activation_required]) do user |> Pleroma.Emails.UserEmail.account_confirmation_email() |> Pleroma.Emails.Mailer.deliver_async() @@ -766,7 +778,7 @@ def follow_all(follower, followeds) do defdelegate following(user), to: FollowingRelationship def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do - deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + deny_follow_blocked = Config.get([:user, :deny_follow_blocked]) cond do followed.deactivated -> @@ -967,7 +979,7 @@ def get_cached_by_nickname(nickname) do end def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do - restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content]) + restrict_to_local = Config.get([:instance, :limit_to_local_content]) cond do is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> @@ -1163,7 +1175,7 @@ defp follow_information_changeset(user, params) do @spec update_follower_count(User.t()) :: {:ok, User.t()} def update_follower_count(%User{} = user) do - if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do + if user.local or !Config.get([:instance, :external_user_synchronization]) do follower_count = FollowingRelationship.follower_count(user) user @@ -1176,7 +1188,7 @@ def update_follower_count(%User{} = user) do @spec update_following_count(User.t()) :: {:ok, User.t()} def update_following_count(%User{local: false} = user) do - if Pleroma.Config.get([:instance, :external_user_synchronization]) do + if Config.get([:instance, :external_user_synchronization]) do {:ok, maybe_fetch_follow_information(user)} else {:ok, user} @@ -1263,7 +1275,7 @@ def unmute(%User{} = muter, %User{} = mutee) do end def subscribe(%User{} = subscriber, %User{} = target) do - deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + deny_follow_blocked = Config.get([:user, :deny_follow_blocked]) if blocks?(target, subscriber) and deny_follow_blocked do {:error, "Could not subscribe: #{target.nickname} is blocking you"} @@ -1309,7 +1321,8 @@ def block(%User{} = blocker, %User{} = blocked) do unsubscribe(blocked, blocker) - if following?(blocked, blocker), do: unfollow(blocked, blocker) + unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true) + if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker) {:ok, blocker} = update_follower_count(blocker) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) @@ -1527,8 +1540,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers) blocked_identifiers, fn blocked_identifier -> with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), - {:ok, _user_block} <- block(blocker, blocked), - {:ok, _} <- ActivityPub.block(blocker, blocked) do + {:ok, _block} <- CommonAPI.block(blocker, blocked) do blocked else err -> @@ -1546,7 +1558,7 @@ def perform(:follow_import, %User{} = follower, followed_identifiers) fn followed_identifier -> with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), {:ok, follower} <- maybe_direct_follow(follower, followed), - {:ok, _} <- ActivityPub.follow(follower, followed) do + {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do followed else err -> @@ -1654,7 +1666,7 @@ def html_filter_policy(%User{no_rich_text: true}) do Pleroma.HTML.Scrubber.TwitterText end - def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) + def html_filter_policy(_), do: Config.get([:markup, :scrub_policy]) def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) @@ -1836,7 +1848,7 @@ defp normalize_tags(tags) do end defp local_nickname_regex do - if Pleroma.Config.get([:instance, :extended_nickname_format]) do + if Config.get([:instance, :extended_nickname_format]) do @extended_local_nickname_regex else @strict_local_nickname_regex @@ -1964,8 +1976,8 @@ def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do def get_mascot(%{mascot: mascot}) when is_nil(mascot) do # use instance-default - config = Pleroma.Config.get([:assets, :mascots]) - default_mascot = Pleroma.Config.get([:assets, :default_mascot]) + config = Config.get([:assets, :mascots]) + default_mascot = Config.get([:assets, :default_mascot]) mascot = Keyword.get(config, default_mascot) %{ @@ -2060,7 +2072,7 @@ def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do def validate_fields(changeset, remote? \\ false) do limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields - limit = Pleroma.Config.get([:instance, limit_name], 0) + limit = Config.get([:instance, limit_name], 0) changeset |> validate_length(:fields, max: limit) @@ -2074,8 +2086,8 @@ def validate_fields(changeset, remote? \\ false) do end defp valid_field?(%{"name" => name, "value" => value}) do - name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) - value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) + name_limit = Config.get([:instance, :account_field_name_length], 255) + value_limit = Config.get([:instance, :account_field_value_length], 255) is_binary(name) && is_binary(value) && String.length(name) <= name_limit && String.length(value) <= value_limit @@ -2085,10 +2097,10 @@ defp valid_field?(_), do: false defp truncate_field(%{"name" => name, "value" => value}) do {name, _chopped} = - String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) + String.split_at(name, Config.get([:instance, :account_field_name_length], 255)) {value, _chopped} = - String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) + String.split_at(value, Config.get([:instance, :account_field_value_length], 255)) %{"name" => name, "value" => value} end @@ -2118,8 +2130,8 @@ def mascot_update(user, url) do def mastodon_settings_update(user, settings) do user - |> cast(%{settings: settings}, [:settings]) - |> validate_required([:settings]) + |> cast(%{mastofe_settings: settings}, [:mastofe_settings]) + |> validate_required([:mastofe_settings]) |> update_and_set_cache() end @@ -2143,7 +2155,7 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do if id not in user.pinned_activities do - max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) + max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) params = %{pinned_activities: user.pinned_activities ++ [id]} user diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex index 4bd55e139..7d9e8a000 100644 --- a/lib/pleroma/user/notification_setting.ex +++ b/lib/pleroma/user/notification_setting.ex @@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do @primary_key false embedded_schema do - field(:followers, :boolean, default: true) - field(:follows, :boolean, default: true) - field(:non_follows, :boolean, default: true) - field(:non_followers, :boolean, default: true) - field(:privacy_option, :boolean, default: false) + field(:block_from_strangers, :boolean, default: false) + field(:hide_notification_contents, :boolean, default: false) end def changeset(schema, params) do schema |> cast(prepare_attrs(params), [ - :followers, - :follows, - :non_follows, - :non_followers, - :privacy_option + :block_from_strangers, + :hide_notification_contents ]) end diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index cec59c372..d4fd31069 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do |> base_query(following) |> filter_blocked_user(for_user) |> filter_invisible_users() + |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) |> trigram_rank(query_string) @@ -68,11 +69,15 @@ defp fts_search(query, query_string) do u in query, where: fragment( + # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work """ - (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?) + ( + setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || + setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B') + ) @@ to_tsquery('simple', ?) """, - u.name, u.nickname, + u.name, ^query_string ) ) @@ -87,15 +92,23 @@ defp to_tsquery(query_string) do |> 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(?, trim(? || ' ' || coalesce(?, '')))", + """ + similarity(?, ?) + + similarity(?, regexp_replace(?, '@.+', '')) + + similarity(?, trim(coalesce(?, ''))) + """, ^query_string, u.nickname, + ^query_string, + u.nickname, + ^query_string, u.name ) } @@ -109,6 +122,10 @@ 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_blocked_user(query, %User{} = blocker) do query |> join(:left, [u], b in Pleroma.UserRelationship, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 7cd3eab39..bc7b5d95a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Filter alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object @@ -321,28 +322,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do end end - @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: - {:ok, Activity.t()} | {:error, any()} - def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do - with {:ok, result} <- - Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do - result - end - end - - defp do_follow(follower, followed, activity_id, local, opts) do - skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) - data = make_follow_data(follower, followed, activity_id) - - with {:ok, activity} <- insert(data, local), - _ <- skip_notify_and_stream || notify_and_stream(activity), - :ok <- maybe_federate(activity) do - {:ok, activity} - else - {:error, error} -> Repo.rollback(error) - end - end - @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) :: {:ok, Activity.t()} | nil | {:error, any()} def unfollow(follower, followed, activity_id \\ nil, local \\ true) do @@ -366,33 +345,6 @@ defp do_unfollow(follower, followed, activity_id, local) do end end - @spec block(User.t(), User.t(), String.t() | nil, boolean()) :: - {:ok, Activity.t()} | {:error, any()} - def block(blocker, blocked, activity_id \\ nil, local \\ true) do - with {:ok, result} <- - Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do - result - end - end - - defp do_block(blocker, blocked, activity_id, local) do - unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - - if unfollow_blocked and fetch_latest_follow(blocker, blocked) do - unfollow(blocker, blocked, nil, local) - end - - block_data = make_block_data(blocker, blocked, activity_id) - - with {:ok, activity} <- insert(block_data, local), - _ <- notify_and_stream(activity), - :ok <- maybe_federate(activity) do - {:ok, activity} - else - {:error, error} -> Repo.rollback(error) - end - end - @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()} def flag( %{ @@ -473,6 +425,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts[:user]) + |> restrict_filtered(opts) |> where( [activity], fragment( @@ -988,6 +941,26 @@ defp restrict_instance(query, %{instance: instance}) do defp restrict_instance(query, _), do: query + defp restrict_filtered(query, %{user: %User{} = user}) do + case Filter.compose_regex(user) do + nil -> + query + + regex -> + from([activity, object] in query, + where: + fragment("not(?->>'content' ~* ?)", object.data, ^regex) or + activity.actor == ^user.ap_id + ) + end + end + + defp restrict_filtered(query, %{blocking_user: %User{} = user}) do + restrict_filtered(query, %{user: user}) + end + + defp restrict_filtered(query, _), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do @@ -1118,6 +1091,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) |> restrict_muted(restrict_muted_opts) + |> restrict_filtered(opts) |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) @@ -1126,6 +1100,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) |> restrict_announce_object_actor(opts) + |> restrict_filtered(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) |> exclude_chat_messages(opts) @@ -1251,6 +1226,8 @@ defp object_to_user_data(data) do end) locked = data["manuallyApprovesFollowers"] || false + capabilities = data["capabilities"] || %{} + accepts_chat_messages = capabilities["acceptsChatMessages"] data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false invisible = data["invisible"] || false @@ -1289,7 +1266,8 @@ defp object_to_user_data(data) do also_known_as: Map.get(data, "alsoKnownAs", []), public_key: public_key, inbox: data["inbox"], - shared_inbox: shared_inbox + shared_inbox: shared_inbox, + accepts_chat_messages: accepts_chat_messages } # nickname can be nil because of virtual actors @@ -1398,6 +1376,31 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do end end + def maybe_handle_clashing_nickname(data) do + nickname = data[:nickname] + + with %User{} = old_user <- User.get_by_nickname(nickname), + {_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do + Logger.info( + "Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{ + data[:ap_id] + }, renaming." + ) + + old_user + |> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"}) + |> User.update_and_set_cache() + else + {:ap_id_comparison, true} -> + Logger.info( + "Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything." + ) + + _ -> + nil + end + end + def make_user_from_ap_id(ap_id) do user = User.get_cached_by_ap_id(ap_id) @@ -1410,6 +1413,8 @@ def make_user_from_ap_id(ap_id) do |> User.remote_user_changeset(data) |> User.update_and_set_cache() else + maybe_handle_clashing_nickname(data) + data |> User.remote_user_changeset() |> Repo.insert() diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 135a5c431..d5f3610ed 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do require Pleroma.Constants + @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} + def follow(follower, followed) do + data = %{ + "id" => Utils.generate_activity_id(), + "actor" => follower.ap_id, + "type" => "Follow", + "object" => followed.ap_id, + "to" => [followed.ap_id] + } + + {:ok, data, []} + end + @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()} def emoji_react(actor, object, emoji) do with {:ok, data, meta} <- object_action(actor, object) do @@ -138,6 +151,18 @@ def update(actor, object) do }, []} end + @spec block(User.t(), User.t()) :: {:ok, map(), keyword()} + def block(blocker, blocked) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "type" => "Block", + "actor" => blocker.ap_id, + "object" => blocked.ap_id, + "to" => [blocked.ap_id] + }, []} + end + @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} def announce(actor, object, options \\ []) do public? = Keyword.get(options, :public, false) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 0270b96ae..b96388489 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -60,7 +60,7 @@ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do if score < 0.8 do {:ok, message} else - {:reject, nil} + {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"} end end diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index 9e7800997..b22464111 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -27,23 +27,25 @@ defp contains_links?(_), do: false @impl true def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do - with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), + with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor), {:contains_links, true} <- {:contains_links, contains_links?(object)}, {:old_user, true} <- {:old_user, old_user?(u)} do {:ok, message} else + {:ok, %User{local: true}} -> + {:ok, message} + {:contains_links, false} -> {:ok, message} {:old_user, false} -> - {:reject, nil} + {:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"} {:error, _} -> - {:reject, nil} + {:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"} e -> - Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") - {:reject, nil} + {:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"} end end diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index f6b2c4415..9ba07b4e3 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -43,7 +43,7 @@ defp delist_message(message, _threshold), do: {:ok, message} defp reject_message(message, threshold) when threshold > 0 do with {_, recipients} <- get_recipient_count(message) do if recipients > threshold do - {:reject, nil} + {:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"} else {:ok, message} end @@ -87,7 +87,7 @@ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message {:ok, message} <- delist_message(message, delist_threshold) do {:ok, message} else - _e -> {:reject, nil} + e -> e end end diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index 88b0d2b39..15e09dcf0 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -24,7 +24,7 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> string_matches?(content, pattern) or string_matches?(summary, pattern) end) do - {:reject, nil} + {:reject, "[KeywordPolicy] Matches with rejected keyword"} else {:ok, message} end @@ -89,8 +89,9 @@ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message {:ok, message} <- check_replace(message) do {:ok, message} else - _e -> - {:reject, nil} + {:reject, nil} -> {:reject, "[KeywordPolicy] "} + {:reject, _} = e -> e + _e -> {:reject, "[KeywordPolicy] "} end end diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex index 06f003921..7910ca131 100644 --- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex @@ -12,8 +12,9 @@ def filter(%{"type" => "Create"} = message) do reject_actors = Pleroma.Config.get([:mrf_mention, :actors], []) recipients = (message["to"] || []) ++ (message["cc"] || []) - if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do - {:reject, nil} + if rejected_mention = + Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do + {:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"} else {:ok, message} end diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex index b0ccb63c8..5f111c72f 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -28,7 +28,7 @@ defp check_date(%{"object" => %{"published" => published}} = message) do defp check_reject(message, actions) do if :reject in actions do - {:reject, nil} + {:reject, "[ObjectAgePolicy]"} else {:ok, message} end @@ -47,9 +47,8 @@ defp check_delist(message, actions) do {:ok, message} else - # Unhandleable error: somebody is messing around, just drop the message. _e -> - {:reject, nil} + {:reject, "[ObjectAgePolicy] Unhandled error"} end else {:ok, message} @@ -69,9 +68,8 @@ defp check_strip_followers(message, actions) do {:ok, message} else - # Unhandleable error: somebody is messing around, just drop the message. _e -> - {:reject, nil} + {:reject, "[ObjectAgePolicy] Unhandled error"} end else {:ok, message} @@ -98,7 +96,7 @@ def filter(message), do: {:ok, message} @impl true def describe do mrf_object_age = - Pleroma.Config.get(:mrf_object_age) + Config.get(:mrf_object_age) |> Enum.into(%{}) {:ok, %{mrf_object_age: mrf_object_age}} diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 3092f3272..0b9ed2224 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -38,7 +38,7 @@ def filter(%{"type" => "Create"} = object) do {:ok, object} true -> - {:reject, nil} + {:reject, "[RejectNonPublic] visibility: #{visibility}"} end end @@ -47,5 +47,5 @@ def filter(object), do: {:ok, object} @impl true def describe, - do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} + do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 9cea6bcf9..b77b8c7b4 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -21,7 +21,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do accepts == [] -> {:ok, object} actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} MRF.subdomain_match?(accepts, actor_host) -> {:ok, object} - true -> {:reject, nil} + true -> {:reject, "[SimplePolicy] host not in accept list"} end end @@ -31,7 +31,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do |> MRF.subdomains_regex() if MRF.subdomain_match?(rejects, actor_host) do - {:reject, nil} + {:reject, "[SimplePolicy] host in reject list"} else {:ok, object} end @@ -114,7 +114,7 @@ defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} |> MRF.subdomains_regex() if MRF.subdomain_match?(report_removal, actor_host) do - {:reject, nil} + {:reject, "[SimplePolicy] host in report_removal list"} else {:ok, object} end @@ -155,11 +155,11 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do %{host: actor_host} = URI.parse(actor) reject_deletes = - Pleroma.Config.get([:mrf_simple, :reject_deletes]) + Config.get([:mrf_simple, :reject_deletes]) |> MRF.subdomains_regex() if MRF.subdomain_match?(reject_deletes, actor_host) do - {:reject, nil} + {:reject, "[SimplePolicy] host in reject_deletes list"} else {:ok, object} end @@ -177,7 +177,9 @@ def filter(%{"actor" => actor} = object) do {:ok, object} <- check_report_removal(actor_info, object) do {:ok, object} else - _e -> {:reject, nil} + {:reject, nil} -> {:reject, "[SimplePolicy]"} + {:reject, _} = e -> e + _ -> {:reject, "[SimplePolicy]"} end end @@ -191,7 +193,9 @@ def filter(%{"id" => actor, "type" => obj_type} = object) {:ok, object} <- check_banner_removal(actor_info, object) do {:ok, object} else - _e -> {:reject, nil} + {:reject, nil} -> {:reject, "[SimplePolicy]"} + {:reject, _} = e -> e + _ -> {:reject, "[SimplePolicy]"} end end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index c310462cb..febabda08 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -134,12 +134,13 @@ defp process_tag( if user.local == true do {:ok, message} else - {:reject, nil} + {:reject, + "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"} end end - defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), - do: {:reject, nil} + defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}), + do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"} defp process_tag(_, message), do: {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex index 651aed70f..1a28f2ba2 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allow_list_policy.ex @@ -14,7 +14,7 @@ defp filter_by_list(%{"actor" => actor} = object, allow_list) do if actor in allow_list do {:ok, object} else - {:reject, nil} + {:reject, "[UserAllowListPolicy] #{actor} not in the list"} end end diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index 6167a74e2..a6c545570 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -11,22 +11,26 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do with {:ok, _} <- filter(child_message) do {:ok, message} else - {:reject, nil} -> - {:reject, nil} + {:reject, _} = e -> e end end def filter(%{"type" => message_type} = message) do with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]), rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]), - true <- - Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type), - false <- - length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type), + {_, true} <- + {:accepted, + Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)}, + {_, false} <- + {:rejected, + length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)}, {:ok, _} <- filter(message["object"]) do {:ok, message} else - _ -> {:reject, nil} + {:reject, _} = e -> e + {:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"} + {:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"} + _ -> {:reject, "[VocabularyPolicy]"} end end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 2c657b467..df926829c 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -13,10 +13,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @@ -24,6 +26,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) + def validate(%{"type" => "Follow"} = object, meta) do + with {:ok, object} <- + object + |> FollowValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "Block"} = block_activity, meta) do + with {:ok, block_activity} <- + block_activity + |> BlockValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + block_activity = stringify_keys(block_activity) + outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks]) + + meta = + if !outgoing_blocks do + Keyword.put(meta, :do_not_federate, true) + else + meta + end + + {:ok, block_activity, meta} + end + end + def validate(%{"type" => "Update"} = update_activity, meta) do with {:ok, update_activity} <- update_activity diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex new file mode 100644 index 000000000..1dde77198 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:object, ObjectValidators.ObjectID) + end + + def cast_data(data) do + %__MODULE__{} + |> cast(data, __schema__(:fields)) + end + + def validate_data(cng) do + cng + |> validate_required([:id, :type, :actor, :to, :cc, :object]) + |> validate_inclusion(:type, ["Block"]) + |> validate_actor_presence() + |> validate_actor_presence(field_name: :object) + end + + def cast_and_validate(data) do + data + |> cast_data + |> validate_data + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index c481d79e0..91b475393 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -93,12 +93,14 @@ def validate_content_or_attachment(cng) do - If both users are in our system - If at least one of the users in this ChatMessage is a local user - If the recipient is not blocking the actor + - If the recipient is explicitly not accepting chat messages """ def validate_local_concern(cng) do with actor_ap <- get_field(cng, :actor), {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, {_, %User{} = recipient} <- {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false}, {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)}, {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do cng @@ -107,6 +109,10 @@ def validate_local_concern(cng) do cng |> add_error(:actor, "actor is blocked by recipient") + {:not_accepting_chats?, true} -> + cng + |> add_error(:to, "recipient does not accept chat messages") + {:local?, false} -> cng |> add_error(:actor, "actor and recipient are both remote") diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex new file mode 100644 index 000000000..ca2724616 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:object, ObjectValidators.ObjectID) + field(:state, :string, default: "pending") + end + + def cast_data(data) do + %__MODULE__{} + |> cast(data, __schema__(:fields)) + end + + def validate_data(cng) do + cng + |> validate_required([:id, :type, :actor, :to, :cc, :object]) + |> validate_inclusion(:type, ["Follow"]) + |> validate_inclusion(:state, ~w{pending reject accept}) + |> validate_actor_presence() + |> validate_actor_presence(field_name: :object) + end + + def cast_and_validate(data) do + data + |> cast_data + |> validate_data + end +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index b70cbd043..d88f7f3ee 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -49,7 +49,8 @@ def is_representable?(%Activity{} = activity) do """ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do Logger.debug("Federating #{id} to #{inbox}") - %{host: host, path: path} = URI.parse(inbox) + + uri = URI.parse(inbox) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) @@ -57,8 +58,8 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa signature = Pleroma.Signature.sign(actor, %{ - "(request-target)": "post #{path}", - host: host, + "(request-target)": "post #{uri.path}", + host: signature_host(uri), "content-length": byte_size(json), digest: digest, date: date @@ -76,8 +77,9 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa {"digest", digest} ] ) do - if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], - do: Instances.set_reachable(inbox) + if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do + Instances.set_reachable(inbox) + end result else @@ -96,6 +98,14 @@ def publish_one(%{actor_id: actor_id} = params) do |> publish_one() end + defp signature_host(%URI{port: port, scheme: scheme, host: host}) do + if port == URI.default_port(scheme) do + host + else + "#{host}:#{port}" + end + end + defp should_federate?(inbox, public) do if public do true diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 484178edd..b09764d2b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -28,7 +28,7 @@ def relay_ap_id do def follow(target_instance) do with %User{} = local_user <- get_actor(), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), - {:ok, activity} <- ActivityPub.follow(local_user, target_user) do + {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index de143b8f0..1d2c296a5 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do collection, and so on. """ alias Pleroma.Activity + alias Pleroma.Activity.Ir.Topics alias Pleroma.Chat alias Pleroma.Chat.MessageReference + alias Pleroma.FollowingRelationship alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -20,6 +22,84 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do def handle(object, meta \\ []) + # Tasks this handle + # - Follows if possible + # - Sends a notification + # - Generates accept or reject if appropriate + def handle( + %{ + data: %{ + "id" => follow_id, + "type" => "Follow", + "object" => followed_user, + "actor" => following_user + } + } = object, + meta + ) do + with %User{} = follower <- User.get_cached_by_ap_id(following_user), + %User{} = followed <- User.get_cached_by_ap_id(followed_user), + {_, {:ok, _}, _, _} <- + {:following, User.follow(follower, followed, :follow_pending), follower, followed} do + if followed.local && !followed.locked do + Utils.update_follow_state_for_all(object, "accept") + FollowingRelationship.update(follower, followed, :follow_accept) + User.update_follower_count(followed) + User.update_following_count(follower) + + %{ + to: [following_user], + actor: followed, + object: follow_id, + local: true + } + |> ActivityPub.accept() + end + else + {:following, {:error, _}, follower, followed} -> + Utils.update_follow_state_for_all(object, "reject") + FollowingRelationship.update(follower, followed, :follow_reject) + + if followed.local do + %{ + to: [follower.ap_id], + actor: followed, + object: follow_id, + local: true + } + |> ActivityPub.reject() + end + + _ -> + nil + end + + {:ok, notifications} = Notification.create_notifications(object, do_send: false) + + meta = + meta + |> add_notifications(notifications) + + updated_object = Activity.get_by_ap_id(follow_id) + + {:ok, updated_object, meta} + end + + # Tasks this handles: + # - Unfollow and block + def handle( + %{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} = + object, + meta + ) do + with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user), + %User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do + User.block(blocker, blocked) + end + + {:ok, object, meta} + end + # Tasks this handles: # - Update the user # @@ -82,7 +162,10 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do if !User.is_internal_user?(user) do Notification.create_notifications(object) - ActivityPub.stream_out(object) + + object + |> Topics.get_activity_topics() + |> Streamer.stream(object) end {:ok, object, meta} @@ -190,14 +273,20 @@ def handle_object_creation(object) do {:ok, object} end - def handle_undoing(%{data: %{"type" => "Like"}} = object) do - with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), - {:ok, _} <- Utils.remove_like_from_object(object, liked_object), - {:ok, _} <- Repo.delete(object) do - :ok + defp undo_like(nil, object), do: delete_object(object) + + defp undo_like(%Object{} = liked_object, object) do + with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do + delete_object(object) end end + def handle_undoing(%{data: %{"type" => "Like"}} = object) do + object.data["object"] + |> Object.get_by_ap_id() + |> undo_like(object) + end + def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), @@ -227,6 +316,11 @@ def handle_undoing( def handle_undoing(object), do: {:error, ["don't know how to handle", object]} + @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()} + defp delete_object(object) do + with {:ok, _} <- Repo.delete(object), do: :ok + end + defp send_notifications(meta) do Keyword.get(meta, :notifications, []) |> Enum.each(fn notification -> diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 4e318e89c..f37bcab3e 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -62,15 +62,17 @@ def fix_summary(%{"summary" => _} = object) do def fix_summary(object), do: Map.put(object, "summary", "") def fix_addressing_list(map, field) do - cond do - is_binary(map[field]) -> - Map.put(map, field, [map[field]]) + addrs = map[field] - is_nil(map[field]) -> - Map.put(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 + Map.put(map, field, []) end end @@ -233,18 +235,24 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm is_map(url) && is_binary(url["href"]) -> url["href"] is_binary(data["url"]) -> data["url"] is_binary(data["href"]) -> data["href"] + true -> nil end - attachment_url = - %{"href" => href} - |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", Map.get(url || %{}, "type")) + if href do + attachment_url = + %{"href" => href} + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", Map.get(url || %{}, "type")) - %{"url" => [attachment_url]} - |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", data["type"]) - |> Maps.put_if_present("name", data["name"]) + %{"url" => [attachment_url]} + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", data["type"]) + |> Maps.put_if_present("name", data["name"]) + else + nil + end end) + |> Enum.filter(& &1) Map.put(object, "attachment", attachments) end @@ -263,12 +271,18 @@ def fix_url(%{"url" => url} = object) when is_map(url) do def fix_url(%{"type" => object_type, "url" => url} = object) when object_type in ["Video", "Audio"] and is_list(url) do - first_element = Enum.at(url, 0) + attachment = + Enum.find(url, fn x -> + media_type = x["mediaType"] || x["mimeType"] || "" - link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) + is_map(x) and String.starts_with?(media_type, ["audio/", "video/"]) + end) + + link_element = + Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end) object - |> Map.put("attachment", [first_element]) + |> Map.put("attachment", [attachment]) |> Map.put("url", link_element["href"]) end @@ -446,12 +460,9 @@ def handle_incoming( when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do actor = Containment.get_actor(data) - data = - Map.put(data, "actor", actor) - |> fix_addressing - with nil <- Activity.get_create_by_object_ap_id(object["id"]), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do + {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor), + data <- Map.put(data, "actor", actor) |> fix_addressing() do object = fix_object(object, options) params = %{ @@ -520,66 +531,6 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, - _options - ) do - with %User{local: true} = followed <- - User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), - {:ok, %User{} = follower} <- - User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), - {:ok, activity} <- - ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do - with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), - {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, - {_, false} <- {:user_locked, User.locked?(followed)}, - {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, - {_, {:ok, _}} <- - {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, - {:ok, _relationship} <- - FollowingRelationship.update(follower, followed, :follow_accept) do - ActivityPub.accept(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - else - {:user_blocked, true} -> - {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - - ActivityPub.reject(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - - {:follow, {:error, _}} -> - {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - - ActivityPub.reject(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - - {:user_locked, true} -> - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending) - :noop - end - - ActivityPub.notify_and_stream(activity) - {:ok, activity} - else - _e -> - :error - end - end - def handle_incoming( %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, _options @@ -673,7 +624,7 @@ def handle_incoming( end def handle_incoming(%{"type" => type} = data, _options) - when type in ["Like", "EmojiReact", "Announce"] do + when type in ~w{Like EmojiReact Announce} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do @@ -684,9 +635,10 @@ def handle_incoming(%{"type" => type} = data, _options) end def handle_incoming( - %{"type" => "Update"} = data, + %{"type" => type} = data, _options - ) do + ) + when type in ~w{Update Block Follow} do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} @@ -765,21 +717,6 @@ def handle_incoming( end end - def handle_incoming( - %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data, - _options - ) do - with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), - {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker), - {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do - User.unfollow(blocker, blocked) - User.block(blocker, blocked) - {:ok, activity} - else - _e -> :error - end - end - def handle_incoming( %{ "type" => "Move", diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 4a02b09a1..3a4564912 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -81,6 +81,15 @@ def render("user.json", %{user: user}) do fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) + capabilities = + if is_boolean(user.accepts_chat_messages) do + %{ + "acceptsChatMessages" => user.accepts_chat_messages + } + else + %{} + end + %{ "id" => user.ap_id, "type" => user.actor_type, @@ -101,7 +110,8 @@ def render("user.json", %{user: user}) do "endpoints" => endpoints, "attachment" => fields, "tag" => emoji_tags, - "discoverable" => user.discoverable + "discoverable" => user.discoverable, + "capabilities" => capabilities } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 453a6842e..343f41caa 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -47,6 +47,10 @@ def is_list?(_), do: false @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + def visible_for_user?(nil, _), do: false + + def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false + def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do user.ap_id in activity.data["to"] || list_ap_id @@ -54,8 +58,6 @@ def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{ |> Pleroma.List.member?(user) end - def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false - def visible_for_user?(%{local: local} = activity, nil) do cfg_key = if local, diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index f9545d895..e5f14269a 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -206,8 +206,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do end end - def user_show(conn, %{"nickname" => nickname}) do - with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do + def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do conn |> put_view(AccountView) |> render("show.json", %{user: user}) @@ -233,11 +233,11 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do |> render("index.json", %{activities: activities, as: :activity}) end - def list_user_statuses(conn, %{"nickname" => nickname} = params) do + def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true godmode = params["godmode"] == "true" || params["godmode"] == true - with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do {_, page_size} = page_params(params) activities = @@ -526,7 +526,7 @@ def disable_mfa(conn, %{"nickname" => nickname}) do @doc "Show a given user's credentials" def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do - with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do conn |> put_view(AccountView) |> render("credentials.json", %{user: user, for: admin}) diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex index 7f60470cb..0df13007f 100644 --- a/lib/pleroma/web/admin_api/controllers/config_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do alias Pleroma.ConfigDB alias Pleroma.Plugs.OAuthScopesPlug - @descriptions Pleroma.Docs.JSON.compile() - plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) @@ -25,7 +23,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation def descriptions(conn, _params) do - descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1) json(conn, descriptions) end diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex index bd9026237..fbfc27d6f 100644 --- a/lib/pleroma/web/api_spec/cast_and_validate.ex +++ b/lib/pleroma/web/api_spec/cast_and_validate.ex @@ -40,7 +40,7 @@ def call(%{private: %{open_api_spex: private_data}} = conn, %{ |> List.first() _ -> - nil + "application/json" end private_data = Map.put(private_data, :operation_id, operation_id) diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex index a258e8421..2a7f1a706 100644 --- a/lib/pleroma/web/api_spec/helpers.ex +++ b/lib/pleroma/web/api_spec/helpers.ex @@ -29,6 +29,10 @@ def request_body(description, schema_ref, opts \\ []) do } end + def admin_api_params do + [Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")] + end + def pagination_params do [ Operation.parameter(:max_id, :query, :string, "Return items older than this ID"), diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 9bde8fc0d..952d9347b 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -61,7 +61,7 @@ def update_credentials_operation do description: "Update the user's display and preferences.", operationId: "AccountController.update_credentials", security: [%{"oAuth" => ["write:accounts"]}], - requestBody: request_body("Parameters", update_creadentials_request(), required: true), + requestBody: request_body("Parameters", update_credentials_request(), required: true), responses: %{ 200 => Operation.response("Account", "application/json", Account), 403 => Operation.response("Error", "application/json", ApiError) @@ -203,14 +203,23 @@ def follow_operation do security: [%{"oAuth" => ["follow", "write:follows"]}], description: "Follow the given account", parameters: [ - %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, - Operation.parameter( - :reblogs, - :query, - BooleanLike, - "Receive this account's reblogs in home timeline? Defaults to true." - ) + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} ], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + reblogs: %Schema{ + type: :boolean, + description: "Receive this account's reblogs in home timeline? Defaults to true.", + default: true + } + } + }, + required: false + ), responses: %{ 200 => Operation.response("Relationship", "application/json", AccountRelationship), 400 => Operation.response("Error", "application/json", ApiError), @@ -438,6 +447,7 @@ defp create_request do } end + # TODO: This is actually a token respone, but there's no oauth operation file yet. defp create_response do %Schema{ title: "AccountCreateResponse", @@ -446,19 +456,25 @@ defp create_response do properties: %{ token_type: %Schema{type: :string}, access_token: %Schema{type: :string}, - scope: %Schema{type: :array, items: %Schema{type: :string}}, - created_at: %Schema{type: :integer, format: :"date-time"} + refresh_token: %Schema{type: :string}, + scope: %Schema{type: :string}, + created_at: %Schema{type: :integer, format: :"date-time"}, + me: %Schema{type: :string}, + expires_in: %Schema{type: :integer} }, example: %{ + "token_type" => "Bearer", "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", + "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz", "created_at" => 1_585_918_714, - "scope" => ["read", "write", "follow", "push"], - "token_type" => "Bearer" + "expires_in" => 600, + "scope" => "read write follow push", + "me" => "https://gensokyo.2hu/users/raymoo" } } end - defp update_creadentials_request do + defp update_credentials_request do %Schema{ title: "AccountUpdateCredentialsRequest", description: "POST body for creating an account", @@ -492,6 +508,11 @@ defp update_creadentials_request do nullable: true, description: "Whether manual approval of follow requests is required." }, + accepts_chat_messages: %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Whether the user accepts receiving chat messages." + }, fields_attributes: %Schema{ nullable: true, oneOf: [ diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex index 7b38a2ef4..3a8380797 100644 --- a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -26,6 +26,7 @@ def show_operation do %Schema{type: :boolean, default: false}, "Get only saved in database settings" ) + | admin_api_params() ], security: [%{"oAuth" => ["read"]}], responses: %{ @@ -41,6 +42,7 @@ def update_operation do summary: "Update config settings", operationId: "AdminAPI.ConfigController.update", security: [%{"oAuth" => ["write"]}], + parameters: admin_api_params(), requestBody: request_body("Parameters", %Schema{ type: :object, @@ -73,6 +75,7 @@ def descriptions_operation do summary: "Get JSON with config descriptions.", operationId: "AdminAPI.ConfigController.descriptions", security: [%{"oAuth" => ["read"]}], + parameters: admin_api_params(), responses: %{ 200 => Operation.response("Config Descriptions", "application/json", %Schema{ diff --git a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex index d3af9db49..801024d75 100644 --- a/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/invite_operation.ex @@ -20,6 +20,7 @@ def index_operation do summary: "Get a list of generated invites", operationId: "AdminAPI.InviteController.index", security: [%{"oAuth" => ["read:invites"]}], + parameters: admin_api_params(), responses: %{ 200 => Operation.response("Invites", "application/json", %Schema{ @@ -51,6 +52,7 @@ def create_operation do summary: "Create an account registration invite token", operationId: "AdminAPI.InviteController.create", security: [%{"oAuth" => ["write:invites"]}], + parameters: admin_api_params(), requestBody: request_body("Parameters", %Schema{ type: :object, @@ -71,6 +73,7 @@ def revoke_operation do summary: "Revoke invite by token", operationId: "AdminAPI.InviteController.revoke", security: [%{"oAuth" => ["write:invites"]}], + parameters: admin_api_params(), requestBody: request_body( "Parameters", @@ -97,6 +100,7 @@ def email_operation do summary: "Sends registration invite via email", operationId: "AdminAPI.InviteController.email", security: [%{"oAuth" => ["write:invites"]}], + parameters: admin_api_params(), requestBody: request_body( "Parameters", diff --git a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex index 0358cfbad..20d033f66 100644 --- a/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/media_proxy_cache_operation.ex @@ -33,6 +33,7 @@ def index_operation do %Schema{type: :integer, default: 50}, "Number of statuses to return" ) + | admin_api_params() ], responses: %{ 200 => success_response() @@ -46,6 +47,7 @@ def delete_operation do summary: "Remove a banned MediaProxy URL from Cachex", operationId: "AdminAPI.MediaProxyCacheController.delete", security: [%{"oAuth" => ["write:media_proxy_caches"]}], + parameters: admin_api_params(), requestBody: request_body( "Parameters", @@ -71,6 +73,7 @@ def purge_operation do summary: "Purge and optionally ban a MediaProxy URL", operationId: "AdminAPI.MediaProxyCacheController.purge", security: [%{"oAuth" => ["write:media_proxy_caches"]}], + parameters: admin_api_params(), requestBody: request_body( "Parameters", diff --git a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex index fbc9f80d7..a75f3e622 100644 --- a/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/oauth_app_operation.ex @@ -36,6 +36,7 @@ def index_operation do %Schema{type: :integer, default: 50}, "Number of apps to return" ) + | admin_api_params() ], responses: %{ 200 => @@ -72,6 +73,7 @@ def create_operation do summary: "Create OAuth App", operationId: "AdminAPI.OAuthAppController.create", requestBody: request_body("Parameters", create_request()), + parameters: admin_api_params(), security: [%{"oAuth" => ["write"]}], responses: %{ 200 => Operation.response("App", "application/json", oauth_app()), @@ -85,7 +87,7 @@ def update_operation do tags: ["Admin", "oAuth Apps"], summary: "Update OAuth App", operationId: "AdminAPI.OAuthAppController.update", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["write"]}], requestBody: request_body("Parameters", update_request()), responses: %{ @@ -103,7 +105,7 @@ def delete_operation do tags: ["Admin", "oAuth Apps"], summary: "Delete OAuth App", operationId: "AdminAPI.OAuthAppController.delete", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["write"]}], responses: %{ 204 => no_content_response(), diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex index 7672cb467..67ee5eee0 100644 --- a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -19,6 +19,7 @@ def index_operation do summary: "List Relays", operationId: "AdminAPI.RelayController.index", security: [%{"oAuth" => ["read"]}], + parameters: admin_api_params(), responses: %{ 200 => Operation.response("Response", "application/json", %Schema{ @@ -41,6 +42,7 @@ def follow_operation do summary: "Follow a Relay", operationId: "AdminAPI.RelayController.follow", security: [%{"oAuth" => ["write:follows"]}], + parameters: admin_api_params(), requestBody: request_body("Parameters", %Schema{ type: :object, @@ -64,6 +66,7 @@ def unfollow_operation do summary: "Unfollow a Relay", operationId: "AdminAPI.RelayController.unfollow", security: [%{"oAuth" => ["write:follows"]}], + parameters: admin_api_params(), requestBody: request_body("Parameters", %Schema{ type: :object, diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex index 15e78bfaf..3bb7ec49e 100644 --- a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex @@ -48,6 +48,7 @@ def index_operation do %Schema{type: :integer, default: 50}, "Number number of log entries per page" ) + | admin_api_params() ], responses: %{ 200 => @@ -71,7 +72,7 @@ def show_operation do tags: ["Admin", "Reports"], summary: "Get an individual report", operationId: "AdminAPI.ReportController.show", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["read:reports"]}], responses: %{ 200 => Operation.response("Report", "application/json", report()), @@ -86,6 +87,7 @@ def update_operation do summary: "Change the state of one or multiple reports", operationId: "AdminAPI.ReportController.update", security: [%{"oAuth" => ["write:reports"]}], + parameters: admin_api_params(), requestBody: request_body("Parameters", update_request(), required: true), responses: %{ 204 => no_content_response(), @@ -100,7 +102,7 @@ def notes_create_operation do tags: ["Admin", "Reports"], summary: "Create report note", operationId: "AdminAPI.ReportController.notes_create", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], requestBody: request_body("Parameters", %Schema{ type: :object, @@ -124,6 +126,7 @@ def notes_delete_operation do parameters: [ Operation.parameter(:report_id, :path, :string, "Report ID"), Operation.parameter(:id, :path, :string, "Note ID") + | admin_api_params() ], security: [%{"oAuth" => ["write:reports"]}], responses: %{ diff --git a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex index 745399b4b..c105838a4 100644 --- a/lib/pleroma/web/api_spec/operations/admin/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/status_operation.ex @@ -55,6 +55,7 @@ def index_operation do %Schema{type: :integer, default: 50}, "Number of statuses to return" ) + | admin_api_params() ], responses: %{ 200 => @@ -71,7 +72,7 @@ def show_operation do tags: ["Admin", "Statuses"], summary: "Show Status", operationId: "AdminAPI.StatusController.show", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["read:statuses"]}], responses: %{ 200 => Operation.response("Status", "application/json", status()), @@ -85,7 +86,7 @@ def update_operation do tags: ["Admin", "Statuses"], summary: "Change the scope of an individual reported status", operationId: "AdminAPI.StatusController.update", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["write:statuses"]}], requestBody: request_body("Parameters", update_request(), required: true), responses: %{ @@ -100,7 +101,7 @@ def delete_operation do tags: ["Admin", "Statuses"], summary: "Delete an individual reported status", operationId: "AdminAPI.StatusController.delete", - parameters: [id_param()], + parameters: [id_param() | admin_api_params()], security: [%{"oAuth" => ["write:statuses"]}], responses: %{ 200 => empty_object_response(), diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 90922c064..97836b2eb 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do alias OpenApiSpex.Operation - alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.FlakeID @@ -40,48 +39,6 @@ def confirmation_resend_operation do } end - def update_avatar_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user avatar image", - operationId: "PleromaAPI.AccountController.update_avatar", - requestBody: - request_body("Parameters", update_avatar_or_background_request(), required: true), - security: [%{"oAuth" => ["write:accounts"]}], - responses: %{ - 200 => update_response(), - 403 => Operation.response("Forbidden", "application/json", ApiError) - } - } - end - - def update_banner_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user banner image", - operationId: "PleromaAPI.AccountController.update_banner", - requestBody: request_body("Parameters", update_banner_request(), required: true), - security: [%{"oAuth" => ["write:accounts"]}], - responses: %{ - 200 => update_response() - } - } - end - - def update_background_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user background image", - operationId: "PleromaAPI.AccountController.update_background", - security: [%{"oAuth" => ["write:accounts"]}], - requestBody: - request_body("Parameters", update_avatar_or_background_request(), required: true), - responses: %{ - 200 => update_response() - } - } - end - def favourites_operation do %Operation{ tags: ["Accounts"], @@ -136,52 +93,4 @@ defp id_param do required: true ) end - - defp update_avatar_or_background_request do - %Schema{ - title: "PleromaAccountUpdateAvatarOrBackgroundRequest", - type: :object, - properties: %{ - img: %Schema{ - nullable: true, - type: :string, - format: :binary, - description: "Image encoded using `multipart/form-data` or an empty string to clear" - } - } - } - end - - defp update_banner_request do - %Schema{ - title: "PleromaAccountUpdateBannerRequest", - type: :object, - properties: %{ - banner: %Schema{ - type: :string, - nullable: true, - format: :binary, - description: "Image encoded using `multipart/form-data` or an empty string to clear" - } - } - } - end - - defp update_response do - Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ - type: :object, - properties: %{ - url: %Schema{ - type: :string, - format: :uri, - nullable: true, - description: "Image URL" - } - }, - example: %{ - "url" => - "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" - } - }) - end end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 0b7fad793..5bd4619d5 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -84,7 +84,7 @@ def delete_operation do operationId: "StatusController.delete", parameters: [id_param()], responses: %{ - 200 => empty_object_response(), + 200 => status_response(), 403 => Operation.response("Forbidden", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index d54e2158d..ca79f0747 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -40,33 +40,72 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do pleroma: %Schema{ type: :object, properties: %{ - allow_following_move: %Schema{type: :boolean}, - background_image: %Schema{type: :string, nullable: true}, + allow_following_move: %Schema{ + type: :boolean, + description: "whether the user allows automatically follow moved following accounts" + }, + background_image: %Schema{type: :string, nullable: true, format: :uri}, chat_token: %Schema{type: :string}, - confirmation_pending: %Schema{type: :boolean}, + confirmation_pending: %Schema{ + type: :boolean, + description: + "whether the user account is waiting on email confirmation to be activated" + }, hide_favorites: %Schema{type: :boolean}, - hide_followers_count: %Schema{type: :boolean}, - hide_followers: %Schema{type: :boolean}, - hide_follows_count: %Schema{type: :boolean}, - hide_follows: %Schema{type: :boolean}, - is_admin: %Schema{type: :boolean}, - is_moderator: %Schema{type: :boolean}, + hide_followers_count: %Schema{ + type: :boolean, + description: "whether the user has follower stat hiding enabled" + }, + hide_followers: %Schema{ + type: :boolean, + description: "whether the user has follower hiding enabled" + }, + hide_follows_count: %Schema{ + type: :boolean, + description: "whether the user has follow stat hiding enabled" + }, + hide_follows: %Schema{ + type: :boolean, + description: "whether the user has follow hiding enabled" + }, + is_admin: %Schema{ + type: :boolean, + description: "whether the user is an admin of the local instance" + }, + is_moderator: %Schema{ + type: :boolean, + description: "whether the user is a moderator of the local instance" + }, skip_thread_containment: %Schema{type: :boolean}, - tags: %Schema{type: :array, items: %Schema{type: :string}}, - unread_conversation_count: %Schema{type: :integer}, + tags: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: + "List of tags being used for things like extra roles or moderation(ie. marking all media as nsfw all)." + }, + unread_conversation_count: %Schema{ + type: :integer, + description: "The count of unread conversations. Only returned to the account owner." + }, notification_settings: %Schema{ type: :object, properties: %{ - followers: %Schema{type: :boolean}, - follows: %Schema{type: :boolean}, - non_followers: %Schema{type: :boolean}, - non_follows: %Schema{type: :boolean}, - privacy_option: %Schema{type: :boolean} + block_from_strangers: %Schema{type: :boolean}, + hide_notification_contents: %Schema{type: :boolean} } }, relationship: AccountRelationship, settings_store: %Schema{ - type: :object + type: :object, + description: + "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" + }, + accepts_chat_messages: %Schema{type: :boolean, nullable: true}, + favicon: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "Favicon image of the user's instance" } } }, @@ -74,16 +113,32 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do type: :object, properties: %{ fields: %Schema{type: :array, items: AccountField}, - note: %Schema{type: :string}, + note: %Schema{ + type: :string, + description: + "Plaintext version of the bio without formatting applied by the backend, used for editing the bio." + }, privacy: VisibilityScope, sensitive: %Schema{type: :boolean}, pleroma: %Schema{ type: :object, properties: %{ actor_type: ActorType, - discoverable: %Schema{type: :boolean}, - no_rich_text: %Schema{type: :boolean}, - show_role: %Schema{type: :boolean} + discoverable: %Schema{ + type: :boolean, + description: + "whether the user allows discovery of the account in search results and other services." + }, + no_rich_text: %Schema{ + type: :boolean, + description: + "whether the HTML tags for rich-text formatting are stripped from all statuses requested from the API." + }, + show_role: %Schema{ + type: :boolean, + description: + "whether the user wants their role (e.g admin, moderator) to be shown" + } } } } @@ -118,16 +173,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "is_admin" => false, "is_moderator" => false, "skip_thread_containment" => false, + "accepts_chat_messages" => true, "chat_token" => "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc", "unread_conversation_count" => 0, "tags" => [], "notification_settings" => %{ - "followers" => true, - "follows" => true, - "non_followers" => true, - "non_follows" => true, - "privacy_option" => false + "block_from_strangers" => false, + "hide_notification_contents" => false }, "relationship" => %{ "blocked_by" => false, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 8b87cb25b..947e42890 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -62,6 +62,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do } }, content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, + text: %Schema{ + type: :string, + description: "Original unformatted content in plain text", + nullable: true + }, created_at: %Schema{ type: :string, format: "date-time", @@ -184,6 +189,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do thread_muted: %Schema{ type: :boolean, description: "`true` if the thread the post belongs to is muted" + }, + parent_visible: %Schema{ + type: :boolean, + description: "`true` if the parent post is visible to the user" } } }, diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9bcb9f587..f849b2e01 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -186,6 +186,7 @@ defp object(draft) do draft.poll ) |> Map.put("emoji", emoji) + |> Map.put("source", draft.status) %__MODULE__{draft | object: object} end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 04e081a8e..4d5b0decf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -25,6 +25,13 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger + def block(blocker, blocked) do + with {:ok, block_data, _} <- Builder.block(blocker, blocked), + {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do + {:ok, block} + end + end + def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), :ok <- validate_chat_content_length(content, !!maybe_attachment), @@ -94,10 +101,14 @@ def unblock(blocker, blocked) do def follow(follower, followed) do timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) - with {:ok, follower} <- User.maybe_direct_follow(follower, followed), - {:ok, activity} <- ActivityPub.follow(follower, followed), + with {:ok, follow_data, _} <- Builder.follow(follower, followed), + {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true), {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do - {:ok, follower, followed, activity} + if activity.data["state"] == "reject" do + {:error, :rejected} + else + {:ok, follower, followed, activity} + end end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 15594125f..9c38b73eb 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -143,7 +143,7 @@ def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data) def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data) when is_list(options) do - limits = Pleroma.Config.get([:instance, :poll_limits]) + limits = Config.get([:instance, :poll_limits]) with :ok <- validate_poll_expiration(expires_in, limits), :ok <- validate_poll_options_amount(options, limits), @@ -502,7 +502,7 @@ def maybe_extract_mentions(_), do: [] def make_report_content_html(nil), do: {:ok, {nil, [], []}} def make_report_content_html(comment) do - max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) + max_size = Config.get([:instance, :max_report_comment_size], 1000) if String.length(comment) <= max_size do {:ok, format_input(comment, "text/plain")} @@ -564,7 +564,7 @@ def validate_character_limit("" = _full_payload, [] = _attachments) do end def validate_character_limit(full_payload, _attachments) do - limit = Pleroma.Config.get([:instance, :limit]) + limit = Config.get([:instance, :limit]) length = String.length(full_payload) if length <= limit do diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex index 0d9d578fc..431ad5485 100644 --- a/lib/pleroma/web/fallback_redirect_controller.ex +++ b/lib/pleroma/web/fallback_redirect_controller.ex @@ -9,6 +9,7 @@ defmodule Fallback.RedirectController do alias Pleroma.User alias Pleroma.Web.Metadata + alias Pleroma.Web.Preload def api_not_implemented(conn, _params) do conn @@ -16,16 +17,7 @@ def api_not_implemented(conn, _params) do |> json(%{error: "Not implemented"}) end - def redirector(conn, _params, code \\ 200) - - # redirect to admin section - # /pleroma/admin -> /pleroma/admin/ - # - def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do - redirect(conn, to: "/pleroma/admin/") - end - - def redirector(conn, _params, code) do + def redirector(conn, _params, code \\ 200) do conn |> put_resp_content_type("text/html") |> send_file(code, index_file_path()) @@ -43,28 +35,33 @@ def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} def redirector_with_meta(conn, params) do {:ok, index_content} = File.read(index_file_path()) - tags = - try do - Metadata.build_tags(params) - rescue - e -> - Logger.error( - "Metadata rendering for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) + tags = build_tags(conn, params) + preloads = preload_data(conn, params) - "" - end - - response = String.replace(index_content, "", tags) + response = + index_content + |> String.replace("", tags <> preloads) conn |> put_resp_content_type("text/html") |> send_resp(200, response) end - def index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") + def redirector_with_preload(conn, %{"path" => ["pleroma", "admin"]}) do + redirect(conn, to: "/pleroma/admin/") + end + + def redirector_with_preload(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + preloads = preload_data(conn, params) + + response = + index_content + |> String.replace("", preloads) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) end def registration_page(conn, params) do @@ -76,4 +73,36 @@ def empty(conn, _params) do |> put_status(204) |> text("") end + + defp index_file_path do + Pleroma.Plugs.InstanceStatic.file_path("index.html") + end + + defp build_tags(conn, params) do + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end + + defp preload_data(conn, params) do + try do + Preload.build_tags(conn, params) + rescue + e -> + Logger.error( + "Preloading for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 7a88a847c..fe5d022f5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -101,12 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do :ok <- TwitterAPI.validate_captcha(app, params), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do - json(conn, %{ - token_type: "Bearer", - access_token: token.token, - scope: app.scopes, - created_at: Token.Utils.format_created_at(token) - }) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else {:error, error} -> json_response(conn, :bad_request, %{error: error}) end @@ -148,6 +144,13 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Enum.filter(fn {_, value} -> not is_nil(value) end) |> Enum.into(%{}) + # We use an empty string as a special value to reset + # avatars, banners, backgrounds + user_image_value = fn + "" -> {:ok, nil} + value -> {:ok, value} + end + user_params = [ :no_rich_text, @@ -160,7 +163,8 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p :show_role, :skip_thread_containment, :allow_following_move, - :discoverable + :discoverable, + :accepts_chat_messages ] |> Enum.reduce(%{}, fn key, acc -> Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) @@ -168,9 +172,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p |> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:bio, params[:note]) |> Maps.put_if_present(:raw_bio, params[:note]) - |> Maps.put_if_present(:avatar, params[:avatar]) - |> Maps.put_if_present(:banner, params[:header]) - |> Maps.put_if_present(:background, params[:pleroma_background_image]) + |> Maps.put_if_present(:avatar, params[:avatar], user_image_value) + |> Maps.put_if_present(:banner, params[:header], user_image_value) + |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value) |> Maps.put_if_present( :raw_fields, params[:fields_attributes], @@ -346,7 +350,7 @@ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do {:error, "Can not follow yourself"} end - def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do + def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do render(conn, "relationship.json", user: follower, target: followed) else @@ -385,8 +389,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do @doc "POST /api/v1/accounts/:id/block" def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, _user_block} <- User.block(blocker, blocked), - {:ok, _activity} <- ActivityPub.block(blocker, blocked) do + with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do render(conn, "relationship.json", user: blocker, target: blocked) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index e50980122..29affa7d5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -44,6 +44,7 @@ def search2(conn, params), do: do_search(:v2, conn, params) def search(conn, params), do: do_search(:v1, conn, params) defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do + query = String.trim(query) options = search_options(params, user) timeout = Keyword.get(Repo.config(), :timeout, 15_000) default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 468b44b67..9bb2ef117 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -172,6 +172,11 @@ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, with_direct_conversation_id: true ) else + {:error, {:reject, message}} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + {:error, message} -> conn |> put_status(:unprocessable_entity) @@ -200,11 +205,16 @@ def show(%{assigns: %{user: user}} = conn, %{id: id}) do @doc "DELETE /api/v1/statuses/:id" def delete(%{assigns: %{user: user}} = conn, %{id: id}) do - with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do - json(conn, %{}) + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + try_render(conn, "show.json", + activity: activity, + for: user, + with_direct_conversation_id: true, + with_source: true + ) else - {:error, :not_found} = e -> e - _e -> render_error(conn, :forbidden, "Can't delete this post") + _e -> {:error, :not_found} end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 4bdd46d7e..ab7b1d6aa 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -88,21 +88,20 @@ def direct(%{assigns: %{user: user}} = conn, params) do ) end + defp restrict_unauthenticated?(true = _local_only) do + Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local]) + end + + defp restrict_unauthenticated?(_) do + Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated]) + end + # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do local_only = params[:local] - cfg_key = - if local_only do - :local - else - :federated - end - - restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) - - if restrict? and is_nil(user) do - render_error(conn, :unauthorized, "authorization required for timeline view") + if is_nil(user) and restrict_unauthenticated?(local_only) do + fail_on_bad_auth(conn) else activities = params @@ -123,6 +122,10 @@ def public(%{assigns: %{user: user}} = conn, params) do end end + defp fail_on_bad_auth(conn) do + render_error(conn, :unauthorized, "authorization required for timeline view") + end + defp hashtag_fetching(params, user, local_only) do tags = [params[:tag], params[:any]] @@ -157,15 +160,20 @@ defp hashtag_fetching(params, user, local_only) do # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do local_only = params[:local] - activities = hashtag_fetching(params, user, local_only) - conn - |> add_link_headers(activities, %{"local" => local_only}) - |> render("index.json", - activities: activities, - for: user, - as: :activity - ) + if is_nil(user) and restrict_unauthenticated?(local_only) do + fail_on_bad_auth(conn) + else + activities = hashtag_fetching(params, user, local_only) + + conn + |> add_link_headers(activities, %{"local" => local_only}) + |> render("index.json", + activities: activities, + for: user, + as: :activity + ) + end end # GET /api/v1/timelines/list/:list_id diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a6e64b4ab..bc9745044 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -204,6 +204,18 @@ defp do_render("show.json", %{user: user} = opts) do %{} end + favicon = + if Pleroma.Config.get([:instances_favicons, :enabled]) do + user + |> Map.get(:ap_id, "") + |> URI.parse() + |> URI.merge("/") + |> Pleroma.Instances.Instance.get_or_update_favicon() + |> MediaProxy.url() + else + nil + end + %{ id: to_string(user.id), username: username_from_nickname(user.nickname), @@ -245,7 +257,9 @@ defp do_render("show.json", %{user: user} = opts) do hide_favorites: user.hide_favorites, relationship: relationship, skip_thread_containment: user.skip_thread_containment, - background_image: image_url(user.background) |> MediaProxy.url() + background_image: image_url(user.background) |> MediaProxy.url(), + accepts_chat_messages: user.accepts_chat_messages, + favicon: favicon } } |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 35c2fc25c..5deb0d7ed 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -34,10 +34,14 @@ def render("show.json", _) do background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), background_image: Keyword.get(instance, :background_image), + chat_limit: Keyword.get(instance, :chat_limit), + description_limit: Keyword.get(instance, :description_limit), pleroma: %{ metadata: %{ + account_activation_required: Keyword.get(instance, :account_activation_required), features: features(), - federation: federation() + federation: federation(), + fields_limits: fields_limits() }, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) } @@ -88,4 +92,13 @@ def federation do end |> Map.put(:enabled, Config.get([:instance, :federating])) end + + def fields_limits do + %{ + max_fields: Config.get([:instance, :max_account_fields]), + max_remote_fields: Config.get([:instance, :max_remote_account_fields]), + name_length: Config.get([:instance, :account_field_name_length]), + value_length: Config.get([:instance, :account_field_value_length]) + } + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 2c49bedb3..fa9d695f3 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy - import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] + import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2] # TODO: Add cached version. defp get_replied_to_activities([]), do: %{} @@ -333,6 +333,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} reblog: nil, card: card, content: content_html, + text: opts[:with_source] && object.data["source"], created_at: created_at, reblogs_count: announcement_count, replies_count: object.data["repliesCount"] || 0, @@ -364,7 +365,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} expires_at: expires_at, direct_conversation_id: direct_conversation_id, thread_muted: thread_muted?, - emoji_reactions: emoji_reactions + emoji_reactions: emoji_reactions, + parent_visible: visible_for_user?(reply_to, opts[:for]) } } end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 077fabe47..dfbfcea6b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -60,22 +60,28 @@ defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) defp whitelisted?(url) do %{host: domain} = URI.parse(url) - mediaproxy_whitelist = Config.get([:media_proxy, :whitelist]) + mediaproxy_whitelist_domains = + [:media_proxy, :whitelist] + |> Config.get() + |> Enum.map(&maybe_get_domain_from_url/1) - upload_base_url_domain = - if !is_nil(Config.get([Upload, :base_url])) do - [URI.parse(Config.get([Upload, :base_url])).host] + whitelist_domains = + if base_url = Config.get([Upload, :base_url]) do + %{host: base_domain} = URI.parse(base_url) + [base_domain | mediaproxy_whitelist_domains] else - [] + mediaproxy_whitelist_domains end - whitelist = mediaproxy_whitelist ++ upload_base_url_domain - - Enum.any?(whitelist, fn pattern -> - String.equivalent?(domain, pattern) - end) + domain in whitelist_domains end + defp maybe_get_domain_from_url("http" <> _ = url) do + URI.parse(url).host + end + + defp maybe_get_domain_from_url(domain), do: domain + def encode_url(url) do base64 = Base.url_encode64(url, @base64_opts) @@ -106,7 +112,7 @@ def filename(url_or_path) do def build_url(sig_base64, url_base64, filename \\ nil) do [ - Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), + Config.get([:media_proxy, :base_url], Web.base_url()), "proxy", sig_base64, url_base64, diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex new file mode 100644 index 000000000..47fa46376 --- /dev/null +++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Nodeinfo.Nodeinfo do + alias Pleroma.Config + alias Pleroma.Stats + alias Pleroma.User + alias Pleroma.Web.Federator.Publisher + alias Pleroma.Web.MastodonAPI.InstanceView + + # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field + # under software. + def get_nodeinfo("2.0") do + stats = Stats.get_stats() + + staff_accounts = + User.all_superusers() + |> Enum.map(fn u -> u.ap_id end) + + federation = InstanceView.federation() + features = InstanceView.features() + + %{ + version: "2.0", + software: %{ + name: Pleroma.Application.name() |> String.downcase(), + version: Pleroma.Application.version() + }, + protocols: Publisher.gather_nodeinfo_protocol_names(), + services: %{ + inbound: [], + outbound: [] + }, + openRegistrations: Config.get([:instance, :registrations_open]), + usage: %{ + users: %{ + total: Map.get(stats, :user_count, 0) + }, + localPosts: Map.get(stats, :status_count, 0) + }, + metadata: %{ + nodeName: Config.get([:instance, :name]), + nodeDescription: Config.get([:instance, :description]), + private: !Config.get([:instance, :public], true), + suggestions: %{ + enabled: false + }, + staffAccounts: staff_accounts, + federation: federation, + pollLimits: Config.get([:instance, :poll_limits]), + postFormats: Config.get([:instance, :allowed_post_formats]), + uploadLimits: %{ + general: Config.get([:instance, :upload_limit]), + avatar: Config.get([:instance, :avatar_upload_limit]), + banner: Config.get([:instance, :banner_upload_limit]), + background: Config.get([:instance, :background_upload_limit]) + }, + fieldsLimits: %{ + maxFields: Config.get([:instance, :max_account_fields]), + maxRemoteFields: Config.get([:instance, :max_remote_account_fields]), + nameLength: Config.get([:instance, :account_field_name_length]), + valueLength: Config.get([:instance, :account_field_value_length]) + }, + accountActivationRequired: Config.get([:instance, :account_activation_required], false), + invitesEnabled: Config.get([:instance, :invites_enabled], false), + mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), + features: features, + restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), + skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) + } + } + end + + def get_nodeinfo("2.1") do + raw_response = get_nodeinfo("2.0") + + updated_software = + raw_response + |> Map.get(:software) + |> Map.put(:repository, Pleroma.Application.repository()) + + raw_response + |> Map.put(:software, updated_software) + |> Map.put(:version, "2.1") + end + + def get_nodeinfo(_version) do + {:error, :missing} + end +end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 721b599d4..8c7a9e565 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -5,12 +5,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do use Pleroma.Web, :controller - alias Pleroma.Config - alias Pleroma.Stats - alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.MastodonAPI.InstanceView + alias Pleroma.Web.Nodeinfo.Nodeinfo def schemas(conn, _params) do response = %{ @@ -29,102 +25,20 @@ def schemas(conn, _params) do json(conn, response) end - # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field - # under software. - def raw_nodeinfo do - stats = Stats.get_stats() - - staff_accounts = - User.all_superusers() - |> Enum.map(fn u -> u.ap_id end) - - features = InstanceView.features() - federation = InstanceView.federation() - - %{ - version: "2.0", - software: %{ - name: Pleroma.Application.name() |> String.downcase(), - version: Pleroma.Application.version() - }, - protocols: Publisher.gather_nodeinfo_protocol_names(), - services: %{ - inbound: [], - outbound: [] - }, - openRegistrations: Config.get([:instance, :registrations_open]), - usage: %{ - users: %{ - total: Map.get(stats, :user_count, 0) - }, - localPosts: Map.get(stats, :status_count, 0) - }, - metadata: %{ - nodeName: Config.get([:instance, :name]), - nodeDescription: Config.get([:instance, :description]), - private: !Config.get([:instance, :public], true), - suggestions: %{ - enabled: false - }, - staffAccounts: staff_accounts, - federation: federation, - pollLimits: Config.get([:instance, :poll_limits]), - postFormats: Config.get([:instance, :allowed_post_formats]), - uploadLimits: %{ - general: Config.get([:instance, :upload_limit]), - avatar: Config.get([:instance, :avatar_upload_limit]), - banner: Config.get([:instance, :banner_upload_limit]), - background: Config.get([:instance, :background_upload_limit]) - }, - fieldsLimits: %{ - maxFields: Config.get([:instance, :max_account_fields]), - maxRemoteFields: Config.get([:instance, :max_remote_account_fields]), - nameLength: Config.get([:instance, :account_field_name_length]), - valueLength: Config.get([:instance, :account_field_value_length]) - }, - accountActivationRequired: Config.get([:instance, :account_activation_required], false), - invitesEnabled: Config.get([:instance, :invites_enabled], false), - mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), - features: features, - restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), - skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) - } - } - end - # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json - def nodeinfo(conn, %{"version" => "2.0"}) do - conn - |> put_resp_header( - "content-type", - "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" - ) - |> json(raw_nodeinfo()) - end + def nodeinfo(conn, %{"version" => version}) do + case Nodeinfo.get_nodeinfo(version) do + {:error, :missing} -> + render_error(conn, :not_found, "Nodeinfo schema version not handled") - def nodeinfo(conn, %{"version" => "2.1"}) do - raw_response = raw_nodeinfo() - - updated_software = - raw_response - |> Map.get(:software) - |> Map.put(:repository, Pleroma.Application.repository()) - - response = - raw_response - |> Map.put(:software, updated_software) - |> Map.put(:version, "2.1") - - conn - |> put_resp_header( - "content-type", - "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8" - ) - |> json(response) - end - - def nodeinfo(conn, _) do - render_error(conn, :not_found, "Nodeinfo schema version not handled") + node_info -> + conn + |> put_resp_header( + "content-type", + "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" + ) + |> json(node_info) + end end end diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index 53e19f82e..f102c93e7 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Token plug(:fetch_session when action in [:show, :verify]) @@ -74,7 +75,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), {:ok, _} <- validates_challenge(user, params), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> conn diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index 41d5578dc..5d87db268 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -5,4 +5,13 @@ defmodule Pleroma.Web.OAuth.MFAView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index c557778ca..7683589cf 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken @@ -233,9 +235,7 @@ def token_exchange( with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, token} <- RefreshToken.grant(token) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> render_invalid_credentials_error(conn) end @@ -247,9 +247,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} {:ok, auth} <- Authorization.get_by_token(app, fixed_token), %User{} = user <- User.get_cached_by_id(auth.user_id), {:ok, token} <- Token.exchange_token(app, auth) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -267,7 +265,7 @@ def token_exchange( {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -290,7 +288,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build_for_client_credentials(token)) + json(conn, OAuthView.render("token.json", %{token: token})) else _error -> handle_token_exchange_error(conn, :invalid_credentails) @@ -548,7 +546,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), defp build_and_response_mfa_token(user, auth) do with {:ok, token} <- MFA.Token.create_token(user, auth) do - Token.Response.build_for_mfa_token(user, token) + MFAView.render("mfa_response.json", %{token: token, user: user}) end end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 94ddaf913..f55247ebd 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,4 +5,26 @@ defmodule Pleroma.Web.OAuth.OAuthView do use Pleroma.Web, :view import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex deleted file mode 100644 index 0e72c31e9..000000000 --- a/lib/pleroma/web/oauth/token/response.ex +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Response do - @moduledoc false - - alias Pleroma.MFA - alias Pleroma.User - alias Pleroma.Web.OAuth.Token.Utils - - @doc false - def build(%User{} = user, token, opts \\ %{}) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: expires_in(), - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - |> Map.merge(opts) - end - - def build_for_client_credentials(token) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - created_at: Utils.format_created_at(token), - expires_in: expires_in(), - scope: Enum.join(token.scopes, " ") - } - end - - def build_for_mfa_token(user, mfa_token) do - %{ - error: "mfa_required", - mfa_token: mfa_token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..563edded7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] - alias Ecto.Changeset alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -35,17 +34,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] ) - plug( - OAuthScopesPlug, - %{scopes: ["write:accounts"]} - # Note: the following actions are not permission-secured in Mastodon: - when action in [ - :update_avatar, - :update_banner, - :update_background - ] - ) - plug( OAuthScopesPlug, %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites @@ -68,56 +56,6 @@ def confirmation_resend(conn, params) do end end - @doc "PATCH /api/v1/pleroma/accounts/update_avatar" - def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do - {:ok, _user} = - user - |> Changeset.change(%{avatar: nil}) - |> User.update_and_set_cache() - - json(conn, %{url: nil}) - end - - def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do - {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) - {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() - %{"url" => [%{"href" => href} | _]} = data - - json(conn, %{url: href}) - end - - @doc "PATCH /api/v1/pleroma/accounts/update_banner" - def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do - with {:ok, _user} <- User.update_banner(user, %{}) do - json(conn, %{url: nil}) - end - end - - def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do - with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), - {:ok, _user} <- User.update_banner(user, object.data) do - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - - @doc "PATCH /api/v1/pleroma/accounts/update_background" - def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do - with {:ok, _user} <- User.update_background(user, %{}) do - json(conn, %{url: nil}) - end - end - - def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do - with {:ok, object} <- ActivityPub.upload(params, type: :background), - {:ok, _user} <- User.update_background(user, object.data) do - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - @doc "GET /api/v1/pleroma/accounts/:id/favourites" def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do render_error(conn, :forbidden, "Can't get favorites") diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex new file mode 100644 index 000000000..90e454468 --- /dev/null +++ b/lib/pleroma/web/preload.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload do + alias Phoenix.HTML + require Logger + + def build_tags(_conn, params) do + preload_data = + Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), %{}, fn parser, acc -> + terms = + params + |> parser.generate_terms() + |> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v))} end) + |> Enum.into(%{}) + + Map.merge(acc, terms) + end) + + rendered_html = + preload_data + |> Jason.encode!() + |> build_script_tag() + |> HTML.safe_to_string() + + rendered_html + end + + def build_script_tag(content) do + HTML.Tag.content_tag(:script, HTML.raw(content), + id: "initial-results", + type: "application/json" + ) + end +end diff --git a/lib/pleroma/web/preload/instance.ex b/lib/pleroma/web/preload/instance.ex new file mode 100644 index 000000000..50d1f3382 --- /dev/null +++ b/lib/pleroma/web/preload/instance.ex @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Instance do + alias Pleroma.Plugs.InstanceStatic + alias Pleroma.Web.MastodonAPI.InstanceView + alias Pleroma.Web.Nodeinfo.Nodeinfo + alias Pleroma.Web.Preload.Providers.Provider + + @behaviour Provider + @instance_url "/api/v1/instance" + @panel_url "/instance/panel.html" + @nodeinfo_url "/nodeinfo/2.0.json" + + @impl Provider + def generate_terms(_params) do + %{} + |> build_info_tag() + |> build_panel_tag() + |> build_nodeinfo_tag() + end + + defp build_info_tag(acc) do + info_data = InstanceView.render("show.json", %{}) + + Map.put(acc, @instance_url, info_data) + end + + defp build_panel_tag(acc) do + instance_path = InstanceStatic.file_path(@panel_url |> to_string()) + + if File.exists?(instance_path) do + panel_data = File.read!(instance_path) + Map.put(acc, @panel_url, panel_data) + else + acc + end + end + + defp build_nodeinfo_tag(acc) do + case Nodeinfo.get_nodeinfo("2.0") do + {:error, _} -> + acc + + nodeinfo_data -> + Map.put(acc, @nodeinfo_url, nodeinfo_data) + end + end +end diff --git a/lib/pleroma/web/preload/provider.ex b/lib/pleroma/web/preload/provider.ex new file mode 100644 index 000000000..7ef595a34 --- /dev/null +++ b/lib/pleroma/web/preload/provider.ex @@ -0,0 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Provider do + @callback generate_terms(map()) :: map() +end diff --git a/lib/pleroma/web/preload/timelines.ex b/lib/pleroma/web/preload/timelines.ex new file mode 100644 index 000000000..57de04051 --- /dev/null +++ b/lib/pleroma/web/preload/timelines.ex @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.Timelines do + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.Preload.Providers.Provider + + @behaviour Provider + @public_url "/api/v1/timelines/public" + + @impl Provider + def generate_terms(params) do + build_public_tag(%{}, params) + end + + def build_public_tag(acc, params) do + if Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated], true) do + acc + else + Map.put(acc, @public_url, public_timeline(params)) + end + end + + defp public_timeline(%{"path" => ["main", "all"]}), do: get_public_timeline(false) + + defp public_timeline(_params), do: get_public_timeline(true) + + defp get_public_timeline(local_only) do + activities = + ActivityPub.fetch_public_activities(%{ + type: ["Create"], + local_only: local_only + }) + + StatusView.render("index.json", activities: activities, for: nil, as: :activity) + end +end diff --git a/lib/pleroma/web/preload/user.ex b/lib/pleroma/web/preload/user.ex new file mode 100644 index 000000000..b3d2e9b8d --- /dev/null +++ b/lib/pleroma/web/preload/user.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.User do + alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.Preload.Providers.Provider + + @behaviour Provider + @account_url_base "/api/v1/accounts" + + @impl Provider + def generate_terms(%{user: user}) do + build_accounts_tag(%{}, user) + end + + def generate_terms(_params), do: %{} + + def build_accounts_tag(acc, %User{} = user) do + account_data = AccountView.render("show.json", %{user: user, for: user}) + Map.put(acc, "#{@account_url_base}/#{user.id}", account_data) + end + + def build_accounts_tag(acc, _), do: acc +end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index cdb827e76..16368485e 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -104,7 +104,7 @@ def build_content(notification, actor, object, mastodon_type \\ nil) def build_content( %{ - user: %{notification_settings: %{privacy_option: true}} + user: %{notification_settings: %{hide_notification_contents: true}} } = notification, _actor, _object, diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ef5ead2da..c8a767935 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -86,7 +86,10 @@ defp parse_url(url) do end try do - {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts) + rich_media_agent = Pleroma.Application.user_agent() <> "; Bot" + + {:ok, %Tesla.Env{body: html}} = + Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts) html |> parse_html() diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 419aa55e4..386308362 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) post("/notifications/read", NotificationController, :mark_as_read) - patch("/accounts/update_avatar", AccountController, :update_avatar) - patch("/accounts/update_banner", AccountController, :update_banner) - patch("/accounts/update_background", AccountController, :update_background) - get("/mascot", MascotController, :show) put("/mascot", MascotController, :update) @@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do scope "/api", Pleroma.Web do pipe_through(:config) - get("/help/test", TwitterAPI.UtilController, :help_test) - post("/help/test", TwitterAPI.UtilController, :help_test) - get("/statusnet/config", TwitterAPI.UtilController, :config) - get("/statusnet/version", TwitterAPI.UtilController, :version) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) end @@ -726,7 +718,7 @@ defmodule Pleroma.Web.Router do get("/registration/:token", RedirectController, :registration_page) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) get("/api*path", RedirectController, :api_not_implemented) - get("/*path", RedirectController, :redirector) + get("/*path", RedirectController, :redirector_with_preload) options("/*path", RedirectController, :empty) end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index d1d2c9b9c..d1d70e556 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -104,7 +104,9 @@ def stream(topics, items) do :ok end - def filtered_by_user?(%User{} = user, %Activity{} = item) do + def filtered_by_user?(user, item, streamed_type \\ :activity) + + def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) @@ -116,6 +118,9 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, + true <- + !(streamed_type == :activity && item.data["type"] == "Announce" && + parent.data["actor"] == user.ap_id), true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- MapSet.disjoint?(recipients, recipient_blocks), %{host: item_host} <- URI.parse(item.actor), @@ -130,8 +135,8 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do end end - def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do - filtered_by_user?(user, activity) + def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do + filtered_by_user?(user, activity, :notification) end defp do_stream("direct", item) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index fd2aee175..f02c4075c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User - alias Pleroma.Web alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger @@ -41,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) - plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) - - def help_test(conn, _params) do - json(conn, "ok") - end - def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do @@ -88,90 +81,14 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end - def config(%{assigns: %{format: "xml"}} = conn, _params) do - instance = Pleroma.Config.get(:instance) - - response = """ - - - #{Keyword.get(instance, :name)} - #{Web.base_url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) - end - - def config(conn, _params) do - instance = Pleroma.Config.get(:instance) - - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), - private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - bool_to_val(Keyword.get(instance, :account_activation_required, false)), - invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), - safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) - } - - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) - Map.put(data, "pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end - - defp bool_to_val(true), do: "1" - defp bool_to_val(_), do: "0" - defp bool_to_val(true, val, _), do: val - defp bool_to_val(_, _, val), do: val - def frontend_configurations(conn, _params) do config = - Pleroma.Config.get(:frontend_configurations, %{}) + Config.get(:frontend_configurations, %{}) |> Enum.into(%{}) json(conn, config) end - def version(%{assigns: %{format: "xml"}} = conn, _params) do - version = Pleroma.Application.named_version() - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, "#{version}") - end - - def version(conn, _params) do - json(conn, Pleroma.Application.named_version()) - end - def emoji(conn, _params) do emoji = Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex index 52054e020..d3bdb4f62 100644 --- a/lib/pleroma/web/twitter_api/views/util_view.ex +++ b/lib/pleroma/web/twitter_api/views/util_view.ex @@ -5,4 +5,18 @@ defmodule Pleroma.Web.TwitterAPI.UtilView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.Web + + def status_net_config(instance) do + """ + + + #{Keyword.get(instance, :name)} + #{Web.base_url()} + #{Keyword.get(instance, :limit)} + #{!Keyword.get(instance, :registrations_open)} + + + """ + end end diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index c3096006e..f739dacb6 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -86,7 +86,7 @@ def initial_state(token, user, custom_emojis) do "video\/mp4" ] }, - settings: user.settings || @default_settings, + settings: user.mastofe_settings || @default_settings, push_subscription: nil, accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)}, custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis), diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 8deeabda0..58226b395 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -11,13 +11,12 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup" @impl Oban.Worker - def perform( - %{ + def perform(%Job{ + args: %{ "op" => "cleanup_attachments", "object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}} - }, - _job - ) do + } + }) do attachments |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end) |> fetch_objects @@ -28,7 +27,7 @@ def perform( {:ok, :success} end - def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} + def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip} defp do_clean({object_ids, attachment_urls}) do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 57c3a9c3a..cec5a7462 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -11,59 +11,59 @@ defmodule Pleroma.Workers.BackgroundWorker do @impl Oban.Worker - def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do + def perform(%Job{args: %{"op" => "deactivate_user", "user_id" => user_id, "status" => status}}) do user = User.get_cached_by_id(user_id) User.perform(:deactivate_async, user, status) end - def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do + def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do user = User.get_cached_by_id(user_id) User.perform(:delete, user) end - def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do + def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) do user = User.get_cached_by_id(user_id) User.perform(:force_password_reset, user) end - def perform( - %{ + def perform(%Job{ + args: %{ "op" => "blocks_import", "blocker_id" => blocker_id, "blocked_identifiers" => blocked_identifiers - }, - _job - ) do + } + }) do blocker = User.get_cached_by_id(blocker_id) {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)} end - def perform( - %{ + def perform(%Job{ + args: %{ "op" => "follow_import", "follower_id" => follower_id, "followed_identifiers" => followed_identifiers - }, - _job - ) do + } + }) do follower = User.get_cached_by_id(follower_id) {:ok, User.perform(:follow_import, follower, followed_identifiers)} end - def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do + def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do MediaProxyWarmingPolicy.perform(:preload, message) end - def perform(%{"op" => "media_proxy_prefetch", "url" => url}, _job) do + def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do MediaProxyWarmingPolicy.perform(:prefetch, url) end - def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, _job) do + def perform(%Job{args: %{"op" => "fetch_data_for_activity", "activity_id" => activity_id}}) do activity = Activity.get_by_id(activity_id) Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity) end - def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do + def perform(%Job{ + args: %{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id} + }) do origin = User.get_cached_by_id(origin_id) target = User.get_cached_by_id(target_id) diff --git a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex index a4c3b9516..d41be4e87 100644 --- a/lib/pleroma/workers/cron/clear_oauth_token_worker.ex +++ b/lib/pleroma/workers/cron/clear_oauth_token_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do alias Pleroma.Web.OAuth.Token @impl Oban.Worker - def perform(_opts, _job) do + def perform(_job) do if Config.get([:oauth2, :clean_expired_tokens], false) do Token.delete_expired_tokens() else diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex index 7f09ff3cf..ee646229f 100644 --- a/lib/pleroma/workers/cron/digest_emails_worker.ex +++ b/lib/pleroma/workers/cron/digest_emails_worker.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do require Logger @impl Oban.Worker - def perform(_opts, _job) do + def perform(_job) do config = Config.get([:email_notifications, :digest]) if config[:active] do diff --git a/lib/pleroma/workers/cron/new_users_digest_worker.ex b/lib/pleroma/workers/cron/new_users_digest_worker.ex index 5c816b3fe..abc8a5e95 100644 --- a/lib/pleroma/workers/cron/new_users_digest_worker.ex +++ b/lib/pleroma/workers/cron/new_users_digest_worker.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do use Pleroma.Workers.WorkerHelper, queue: "new_users_digest" @impl Oban.Worker - def perform(_args, _job) do + def perform(_job) do if Pleroma.Config.get([Pleroma.Emails.NewUsersDigestEmail, :enabled]) do today = NaiveDateTime.utc_now() |> Timex.beginning_of_day() diff --git a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex index 84b3b84de..e926c5dc8 100644 --- a/lib/pleroma/workers/cron/purge_expired_activities_worker.ex +++ b/lib/pleroma/workers/cron/purge_expired_activities_worker.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do @interval :timer.minutes(1) @impl Oban.Worker - def perform(_opts, _job) do + def perform(_job) do if Config.get([ActivityExpiration, :enabled]) do Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1) else diff --git a/lib/pleroma/workers/cron/stats_worker.ex b/lib/pleroma/workers/cron/stats_worker.ex index e9b8d59c4..e54bd9a7f 100644 --- a/lib/pleroma/workers/cron/stats_worker.ex +++ b/lib/pleroma/workers/cron/stats_worker.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Workers.Cron.StatsWorker do use Oban.Worker, queue: "background" @impl Oban.Worker - def perform(_opts, _job) do + def perform(_job) do Pleroma.Stats.do_collect() end end diff --git a/lib/pleroma/workers/mailer_worker.ex b/lib/pleroma/workers/mailer_worker.ex index 6955338a5..32273cfa5 100644 --- a/lib/pleroma/workers/mailer_worker.ex +++ b/lib/pleroma/workers/mailer_worker.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Workers.MailerWorker do use Pleroma.Workers.WorkerHelper, queue: "mailer" @impl Oban.Worker - def perform(%{"op" => "email", "encoded_email" => encoded_email, "config" => config}, _job) do + def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do encoded_email |> Base.decode64!() |> :erlang.binary_to_term() diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex index daf79efc0..e739c3cd0 100644 --- a/lib/pleroma/workers/publisher_worker.ex +++ b/lib/pleroma/workers/publisher_worker.ex @@ -8,17 +8,17 @@ defmodule Pleroma.Workers.PublisherWorker do use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing" - def backoff(attempt) when is_integer(attempt) do + def backoff(%Job{attempt: attempt}) when is_integer(attempt) do Pleroma.Workers.WorkerHelper.sidekiq_backoff(attempt, 5) end @impl Oban.Worker - def perform(%{"op" => "publish", "activity_id" => activity_id}, _job) do + def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do activity = Activity.get_by_id(activity_id) Federator.perform(:publish, activity) end - def perform(%{"op" => "publish_one", "module" => module_name, "params" => params}, _job) do + def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end) Federator.perform(:publish_one, String.to_atom(module_name), params) end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index f7a7124f3..1b97af1a8 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Workers.ReceiverWorker do use Pleroma.Workers.WorkerHelper, queue: "federator_incoming" @impl Oban.Worker - def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do + def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do Federator.perform(:incoming_ap_doc, params) end end diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index ec6534f21..27e2e3386 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -8,13 +8,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher" @impl Oban.Worker - def perform( - %{ - "op" => "fetch_remote", - "id" => id - } = args, - _job - ) do + def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"]) end end diff --git a/lib/pleroma/workers/scheduled_activity_worker.ex b/lib/pleroma/workers/scheduled_activity_worker.ex index 97d1efbfb..dd9986fe4 100644 --- a/lib/pleroma/workers/scheduled_activity_worker.ex +++ b/lib/pleroma/workers/scheduled_activity_worker.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Workers.ScheduledActivityWorker do require Logger @impl Oban.Worker - def perform(%{"activity_id" => activity_id}, _job) do + def perform(%Job{args: %{"activity_id" => activity_id}}) do if Config.get([ScheduledActivity, :enabled]) do case Pleroma.Repo.get(ScheduledActivity, activity_id) do %ScheduledActivity{} = scheduled_activity -> diff --git a/lib/pleroma/workers/transmogrifier_worker.ex b/lib/pleroma/workers/transmogrifier_worker.ex index 11239ca5e..15f36375c 100644 --- a/lib/pleroma/workers/transmogrifier_worker.ex +++ b/lib/pleroma/workers/transmogrifier_worker.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Workers.TransmogrifierWorker do use Pleroma.Workers.WorkerHelper, queue: "transmogrifier" @impl Oban.Worker - def perform(%{"op" => "user_upgrade", "user_id" => user_id}, _job) do + def perform(%Job{args: %{"op" => "user_upgrade", "user_id" => user_id}}) do user = User.get_cached_by_id(user_id) Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user) end diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 58ad25e39..0cfdc6a6f 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Workers.WebPusherWorker do use Pleroma.Workers.WorkerHelper, queue: "web_push" @impl Oban.Worker - def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) do + def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do notification = Notification |> Repo.get(notification_id) diff --git a/lib/pleroma/workers/worker_helper.ex b/lib/pleroma/workers/worker_helper.ex index d1f90c35b..7d1289be2 100644 --- a/lib/pleroma/workers/worker_helper.ex +++ b/lib/pleroma/workers/worker_helper.ex @@ -32,6 +32,8 @@ defmacro __using__(opts) do queue: unquote(queue), max_attempts: 1 + alias Oban.Job + def enqueue(op, params, worker_args \\ []) do params = Map.merge(%{"op" => op}, params) queue_atom = String.to_atom(unquote(queue)) @@ -39,7 +41,7 @@ def enqueue(op, params, worker_args \\ []) do unquote(caller_module) |> apply(:new, [params, worker_args]) - |> Pleroma.Repo.insert() + |> Oban.insert() end end end diff --git a/mix.exs b/mix.exs index c773a3162..9886de666 100644 --- a/mix.exs +++ b/mix.exs @@ -90,8 +90,6 @@ defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] defp warnings_as_errors(:prod), do: false - # Uncomment this if you need testing configurable_from_database logic - # defp warnings_as_errors(:dev), do: false defp warnings_as_errors(_), do: true # Specifies OAuth dependencies. @@ -122,9 +120,9 @@ defp deps do {:phoenix_pubsub, "~> 1.1"}, {:phoenix_ecto, "~> 4.0"}, {:ecto_enum, "~> 1.4"}, - {:ecto_sql, "~> 3.3.2"}, + {:ecto_sql, "~> 3.4.4"}, {:postgrex, ">= 0.13.5"}, - {:oban, "~> 1.2"}, + {:oban, "~> 2.0.0"}, {:gettext, "~> 0.15"}, {:pbkdf2_elixir, "~> 1.0"}, {:bcrypt_elixir, "~> 2.0"}, @@ -137,13 +135,11 @@ defp deps do {:poison, "~> 3.0", override: true}, # {:tesla, "~> 1.3", override: true}, {:tesla, - git: "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", - ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b", - override: true}, + github: "teamon/tesla", ref: "af3707078b10793f6a534938e56b963aff82fe3c", override: true}, {:castore, "~> 0.1"}, {:cowlib, "~> 2.8", override: true}, {:gun, - github: "ninenines/gun", ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc", override: true}, + github: "ninenines/gun", ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", override: true}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, {:ex_aws, "~> 2.1"}, @@ -159,7 +155,10 @@ defp deps do {:cors_plug, "~> 1.5"}, {:ex_doc, "~> 0.21", only: :dev, runtime: false}, {:web_push_encryption, "~> 0.2.1"}, - {:swoosh, "~> 0.23.2"}, + {:swoosh, + git: "https://github.com/swoosh/swoosh", + ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5", + override: true}, {:phoenix_swoosh, "~> 0.2"}, {:gen_smtp, "~> 0.13"}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}, @@ -173,6 +172,7 @@ defp deps do ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"}, {:telemetry, "~> 0.3"}, {:poolboy, "~> 1.5"}, + {:prometheus, "~> 4.6"}, {:prometheus_ex, "~> 3.0"}, {:prometheus_plugs, "~> 1.1"}, {:prometheus_phoenix, "~> 1.3"}, @@ -187,6 +187,9 @@ defp deps do {:plug_static_index_html, "~> 1.0.0"}, {:excoveralls, "~> 0.12.1", only: :test}, {:flake_id, "~> 0.1.0"}, + {:concurrent_limiter, + git: "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter", + ref: "8eee96c6ba39b9286ec44c51c52d9f2758951365"}, {:remote_ip, git: "https://git.pleroma.social/pleroma/remote_ip.git", ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"}, diff --git a/mix.lock b/mix.lock index 458cda6cf..150d14875 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, + "concurrent_limiter": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/concurrent_limiter", "8eee96c6ba39b9286ec44c51c52d9f2758951365", [ref: "8eee96c6ba39b9286ec44c51c52d9f2758951365"]}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9af027d20dc12dd0c4345a6b87247e0c62965871feea0bfecf9764648b02cc69"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, @@ -22,13 +23,13 @@ "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "f63a705f92c26955977ee62a313012e309a4d77a", [ref: "f63a705f92c26955977ee62a313012e309a4d77a"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"}, + "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"}, + "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, "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_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"}, + "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, @@ -48,7 +49,7 @@ "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"}, - "gun": {:git, "https://github.com/ninenines/gun.git", "e1a69b36b180a574c0ac314ced9613fdd52312cc", [ref: "e1a69b36b180a574c0ac314ced9613fdd52312cc"]}, + "gun": {:git, "https://github.com/ninenines/gun.git", "921c47146b2d9567eac7e9a4d2ccc60fffd4f327", [ref: "921c47146b2d9567eac7e9a4d2ccc60fffd4f327"]}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, @@ -75,7 +76,7 @@ "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"}, + "oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"}, "open_api_spex": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/open_api_spex.git", "f296ac0924ba3cf79c7a588c4c252889df4c2edd", [ref: "f296ac0924ba3cf79c7a588c4c252889df4c2edd"]}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"}, @@ -90,9 +91,9 @@ "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.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"}, + "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, "pot": {:hex, :pot, "0.10.2", "9895c83bcff8cd22d9f5bc79dfc88a188176b261b618ad70d93faf5c5ca36e67", [:rebar3], [], "hexpm", "ac589a8e296b7802681e93cd0a436faec117ea63e9916709c628df31e17e91e2"}, - "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"}, + "prometheus": {:hex, :prometheus, "4.6.0", "20510f381db1ccab818b4cf2fac5fa6ab5cc91bc364a154399901c001465f46f", [:mix, :rebar3], [], "hexpm", "4905fd2992f8038eccd7aa0cd22f40637ed618c0bed1f75c05aacec15b7545de"}, "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": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"}, "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"}, @@ -104,10 +105,10 @@ "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, - "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"}, + "swoosh": {:git, "https://github.com/swoosh/swoosh", "c96e0ca8a00d8f211ec1f042a4626b09f249caa5", [ref: "c96e0ca8a00d8f211ec1f042a4626b09f249caa5"]}, "syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, - "tesla": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/tesla.git", "61b7503cef33f00834f78ddfafe0d5d9dec2270b", [ref: "61b7503cef33f00834f78ddfafe0d5d9dec2270b"]}, + "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, + "tesla": {:git, "https://github.com/teamon/tesla.git", "af3707078b10793f6a534938e56b963aff82fe3c", [ref: "af3707078b10793f6a534938e56b963aff82fe3c"]}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, "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.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"}, diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 0e1cf37eb..e337226a7 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -90,110 +90,100 @@ msgid "must be equal to %{number}" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:421 +#: lib/pleroma/web/common_api/common_api.ex:505 msgid "Account not found" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:249 +#: lib/pleroma/web/common_api/common_api.ex:339 msgid "Already voted" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:360 +#: lib/pleroma/web/oauth/oauth_controller.ex:359 msgid "Bad request" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:426 msgid "Can't delete object" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 -msgid "Can't delete this post" -msgstr "" - -#, elixir-format -#: lib/pleroma/web/controller_helper.ex:95 -#: lib/pleroma/web/controller_helper.ex:101 +#: lib/pleroma/web/controller_helper.ex:105 +#: lib/pleroma/web/controller_helper.ex:111 msgid "Can't display this activity" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227 -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:285 msgid "Can't find user" msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 +#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:61 msgid "Can't get favorites" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:438 msgid "Can't like object" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/utils.ex:556 +#: lib/pleroma/web/common_api/utils.ex:563 msgid "Cannot post an empty status without attachments" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/utils.ex:504 +#: lib/pleroma/web/common_api/utils.ex:511 msgid "Comment must be up to %{max_size} characters" msgstr "" #, elixir-format -#: lib/pleroma/config/config_db.ex:222 +#: lib/pleroma/config/config_db.ex:191 msgid "Config with params %{params} not found" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:95 +#: lib/pleroma/web/common_api/common_api.ex:181 +#: lib/pleroma/web/common_api/common_api.ex:185 msgid "Could not delete" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:141 +#: lib/pleroma/web/common_api/common_api.ex:231 msgid "Could not favorite" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:370 +#: lib/pleroma/web/common_api/common_api.ex:453 msgid "Could not pin" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:112 -msgid "Could not repeat" -msgstr "" - -#, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:188 +#: lib/pleroma/web/common_api/common_api.ex:278 msgid "Could not unfavorite" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:380 +#: lib/pleroma/web/common_api/common_api.ex:463 msgid "Could not unpin" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:126 +#: lib/pleroma/web/common_api/common_api.ex:216 msgid "Could not unrepeat" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:428 -#: lib/pleroma/web/common_api/common_api.ex:437 +#: lib/pleroma/web/common_api/common_api.ex:512 +#: lib/pleroma/web/common_api/common_api.ex:521 msgid "Could not update state" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:207 msgid "Error." msgstr "" @@ -203,8 +193,8 @@ msgid "Invalid CAPTCHA" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 -#: lib/pleroma/web/oauth/oauth_controller.ex:569 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:116 +#: lib/pleroma/web/oauth/oauth_controller.ex:568 msgid "Invalid credentials" msgstr "" @@ -214,22 +204,22 @@ msgid "Invalid credentials." msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:265 +#: lib/pleroma/web/common_api/common_api.ex:355 msgid "Invalid indices" msgstr "" #, elixir-format -#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 +#: lib/pleroma/web/admin_api/controllers/fallback_controller.ex:29 msgid "Invalid parameters" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/utils.ex:411 +#: lib/pleroma/web/common_api/utils.ex:414 msgid "Invalid password." msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220 msgid "Invalid request" msgstr "" @@ -239,44 +229,44 @@ msgid "Kocaptcha service unavailable" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112 msgid "Missing parameters" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/utils.ex:540 +#: lib/pleroma/web/common_api/utils.ex:547 msgid "No such conversation" msgstr "" #, elixir-format -#: lib/pleroma/web/admin_api/admin_api_controller.ex:439 -#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 +#: 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 msgid "No such permission_group" msgstr "" #, elixir-format -#: lib/pleroma/plugs/uploaded_media.ex:74 -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135 -#: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143 +#: 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 msgid "Not found" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:241 +#: lib/pleroma/web/common_api/common_api.ex:331 msgid "Poll's author can't vote" msgstr "" #, elixir-format #: 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:290 +#: 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 msgid "Record not found" msgstr "" #, elixir-format -#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 -#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32 +#: 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 msgid "Something went wrong" msgstr "" @@ -287,7 +277,7 @@ msgid "The message visibility must be direct" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/utils.ex:566 +#: lib/pleroma/web/common_api/utils.ex:573 msgid "The status is over the character limit" msgstr "" @@ -302,65 +292,65 @@ msgid "Throttled" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:266 +#: lib/pleroma/web/common_api/common_api.ex:356 msgid "Too many choices" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443 msgid "Unhandled activity type" msgstr "" #, elixir-format -#: lib/pleroma/web/admin_api/admin_api_controller.ex:536 +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:485 msgid "You can't revoke your own admin status." msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:218 -#: lib/pleroma/web/oauth/oauth_controller.ex:309 +#: lib/pleroma/web/oauth/oauth_controller.ex:221 +#: lib/pleroma/web/oauth/oauth_controller.ex:308 msgid "Your account is currently disabled" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:180 -#: lib/pleroma/web/oauth/oauth_controller.ex:332 +#: lib/pleroma/web/oauth/oauth_controller.ex:183 +#: lib/pleroma/web/oauth/oauth_controller.ex:331 msgid "Your login is missing a confirmed e-mail address" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390 msgid "can't read inbox of %{nickname} as %{as_nickname}" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473 msgid "can't update outbox of %{nickname} as %{as_nickname}" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:388 +#: lib/pleroma/web/common_api/common_api.ex:471 msgid "conversation is already muted" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316 -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492 msgid "error" msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 +#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32 msgid "mascots can only be images" msgstr "" #, elixir-format -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 +#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62 msgid "not found" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:395 +#: lib/pleroma/web/oauth/oauth_controller.ex:394 msgid "Bad OAuth request." msgstr "" @@ -375,17 +365,17 @@ msgid "CAPTCHA expired" msgstr "" #, elixir-format -#: lib/pleroma/plugs/uploaded_media.ex:55 +#: lib/pleroma/plugs/uploaded_media.ex:57 msgid "Failed" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:411 +#: lib/pleroma/web/oauth/oauth_controller.ex:410 msgid "Failed to authenticate: %{message}." msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:442 +#: lib/pleroma/web/oauth/oauth_controller.ex:441 msgid "Failed to set up user account." msgstr "" @@ -395,7 +385,7 @@ msgid "Insufficient permissions: %{permissions}." msgstr "" #, elixir-format -#: lib/pleroma/plugs/uploaded_media.ex:94 +#: lib/pleroma/plugs/uploaded_media.ex:104 msgid "Internal Error" msgstr "" @@ -411,12 +401,12 @@ msgid "Invalid answer data" msgstr "" #, elixir-format -#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 +#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:33 msgid "Nodeinfo schema version not handled" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:169 +#: lib/pleroma/web/oauth/oauth_controller.ex:172 msgid "This action is outside the authorized scopes" msgstr "" @@ -426,13 +416,13 @@ msgid "Unknown error, please check the details and try again." msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:116 -#: lib/pleroma/web/oauth/oauth_controller.ex:155 +#: lib/pleroma/web/oauth/oauth_controller.ex:119 +#: lib/pleroma/web/oauth/oauth_controller.ex:158 msgid "Unlisted redirect_uri." msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:391 +#: lib/pleroma/web/oauth/oauth_controller.ex:390 msgid "Unsupported OAuth provider: %{provider}." msgstr "" @@ -452,12 +442,12 @@ msgid "CAPTCHA Error" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:200 +#: lib/pleroma/web/common_api/common_api.ex:290 msgid "Could not add reaction emoji" msgstr "" #, elixir-format -#: lib/pleroma/web/common_api/common_api.ex:211 +#: lib/pleroma/web/common_api/common_api.ex:301 msgid "Could not remove reaction emoji" msgstr "" @@ -472,39 +462,45 @@ msgid "List not found" msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123 msgid "Missing parameter: %{name}" msgstr "" #, elixir-format -#: lib/pleroma/web/oauth/oauth_controller.ex:207 -#: lib/pleroma/web/oauth/oauth_controller.ex:322 +#: lib/pleroma/web/oauth/oauth_controller.ex:210 +#: lib/pleroma/web/oauth/oauth_controller.ex:321 msgid "Password reset is required" msgstr "" #, elixir-format #: lib/pleroma/tests/auth_test_controller.ex:9 -#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6 -#: lib/pleroma/web/controller_helper.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/emoji_api_controller.ex:2 -#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:6 lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex:6 +#: 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 @@ -519,46 +515,56 @@ msgid "Two-factor authentication enabled, you must use a access token." msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210 msgid "Unexpected error occurred while adding file to pack." msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:138 msgid "Unexpected error occurred while creating pack." msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:278 msgid "Unexpected error occurred while removing file from pack." msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:250 msgid "Unexpected error occurred while updating file in pack." msgstr "" #, elixir-format -#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179 +#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:179 msgid "Unexpected error occurred while updating pack metadata." msgstr "" -#, elixir-format -#: lib/pleroma/plugs/user_is_admin_plug.ex:40 -msgid "User is not an admin or OAuth admin scope is not granted." -msgstr "" - #, elixir-format #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 msgid "Web push subscription is disabled on this Pleroma instance" msgstr "" #, elixir-format -#: lib/pleroma/web/admin_api/admin_api_controller.ex:502 +#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451 msgid "You can't revoke your own admin/moderator status." msgstr "" #, elixir-format -#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105 +#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126 msgid "authorization required for timeline view" msgstr "" + +#, elixir-format +#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24 +msgid "Access denied" +msgstr "" + +#, elixir-format +#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282 +msgid "This API requires an authenticated user" +msgstr "" + +#, elixir-format +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 +msgid "User is not an admin." +msgstr "" diff --git a/priv/gettext/it/LC_MESSAGES/errors.po b/priv/gettext/it/LC_MESSAGES/errors.po index 726be628b..cd0cd6c65 100644 --- a/priv/gettext/it/LC_MESSAGES/errors.po +++ b/priv/gettext/it/LC_MESSAGES/errors.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-19 14:33+0000\n" -"PO-Revision-Date: 2020-06-19 20:38+0000\n" +"PO-Revision-Date: 2020-07-09 14:40+0000\n" "Last-Translator: Ben Is \n" "Language-Team: Italian \n" @@ -29,258 +29,258 @@ msgstr "non può essere nullo" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" -msgstr "" +msgstr "è stato già creato" ## From Ecto.Changeset.put_change/3 msgid "is invalid" -msgstr "" +msgstr "non è valido" ## From Ecto.Changeset.validate_format/3 msgid "has invalid format" -msgstr "" +msgstr "è in un formato invalido" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "ha una voce invalida" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" -msgstr "" +msgstr "è vietato" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "non corrisponde alla verifica" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" -msgstr "" +msgstr "è ancora associato con questa voce" msgid "are still associated with this entry" -msgstr "" +msgstr "sono ancora associati con questa voce" ## From Ecto.Changeset.validate_length/3 msgid "should be %{count} character(s)" msgid_plural "should be %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe essere %{count} carattere" +msgstr[1] "dovrebbero essere %{count} caratteri" msgid "should have %{count} item(s)" msgid_plural "should have %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe avere %{count} voce" +msgstr[1] "dovrebbe avere %{count} voci" msgid "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe contenere almeno %{count} carattere" +msgstr[1] "dovrebbe contenere almeno %{count} caratteri" msgid "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe avere almeno %{count} voce" +msgstr[1] "dovrebbe avere almeno %{count} voci" msgid "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe avere al massimo %{count} carattere" +msgstr[1] "dovrebbe avere al massimo %{count} caratteri" msgid "should have at most %{count} item(s)" msgid_plural "should have at most %{count} item(s)" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "dovrebbe avere al massimo %{count} voce" +msgstr[1] "dovrebbe avere al massimo %{count} voci" ## From Ecto.Changeset.validate_number/3 msgid "must be less than %{number}" -msgstr "" +msgstr "dev'essere minore di %{number}" msgid "must be greater than %{number}" -msgstr "" +msgstr "dev'essere maggiore di %{number}" msgid "must be less than or equal to %{number}" -msgstr "" +msgstr "dev'essere minore o uguale a %{number}" msgid "must be greater than or equal to %{number}" -msgstr "" +msgstr "dev'essere maggiore o uguale a %{number}" msgid "must be equal to %{number}" -msgstr "" +msgstr "dev'essere uguale a %{number}" #: lib/pleroma/web/common_api/common_api.ex:421 #, elixir-format msgid "Account not found" -msgstr "" +msgstr "Profilo non trovato" #: lib/pleroma/web/common_api/common_api.ex:249 #, elixir-format msgid "Already voted" -msgstr "" +msgstr "Hai già votato" #: lib/pleroma/web/oauth/oauth_controller.ex:360 #, elixir-format msgid "Bad request" -msgstr "" +msgstr "Richiesta invalida" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 #, elixir-format msgid "Can't delete object" -msgstr "" +msgstr "Non puoi eliminare quest'oggetto" #: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 #, elixir-format msgid "Can't delete this post" -msgstr "" +msgstr "Non puoi eliminare questo messaggio" #: lib/pleroma/web/controller_helper.ex:95 #: lib/pleroma/web/controller_helper.ex:101 #, elixir-format msgid "Can't display this activity" -msgstr "" +msgstr "Non puoi vedere questo elemento" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254 #, elixir-format msgid "Can't find user" -msgstr "" +msgstr "Non trovo questo utente" #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 #, elixir-format msgid "Can't get favorites" -msgstr "" +msgstr "Non posso ricevere i gradimenti" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 #, elixir-format msgid "Can't like object" -msgstr "" +msgstr "Non posso gradire quest'oggetto" #: lib/pleroma/web/common_api/utils.ex:556 #, elixir-format msgid "Cannot post an empty status without attachments" -msgstr "" +msgstr "Non puoi pubblicare un messaggio vuoto senza allegati" #: lib/pleroma/web/common_api/utils.ex:504 #, elixir-format msgid "Comment must be up to %{max_size} characters" -msgstr "" +msgstr "I commenti posso al massimo consistere di %{max_size} caratteri" #: lib/pleroma/config/config_db.ex:222 #, elixir-format msgid "Config with params %{params} not found" -msgstr "" +msgstr "Configurazione con parametri %{max_size} non trovata" #: lib/pleroma/web/common_api/common_api.ex:95 #, elixir-format msgid "Could not delete" -msgstr "" +msgstr "Non eliminato" #: lib/pleroma/web/common_api/common_api.ex:141 #, elixir-format msgid "Could not favorite" -msgstr "" +msgstr "Non gradito" #: lib/pleroma/web/common_api/common_api.ex:370 #, elixir-format msgid "Could not pin" -msgstr "" +msgstr "Non intestato" #: lib/pleroma/web/common_api/common_api.ex:112 #, elixir-format msgid "Could not repeat" -msgstr "" +msgstr "Non ripetuto" #: lib/pleroma/web/common_api/common_api.ex:188 #, elixir-format msgid "Could not unfavorite" -msgstr "" +msgstr "Non sgradito" #: lib/pleroma/web/common_api/common_api.ex:380 #, elixir-format msgid "Could not unpin" -msgstr "" +msgstr "Non de-intestato" #: lib/pleroma/web/common_api/common_api.ex:126 #, elixir-format msgid "Could not unrepeat" -msgstr "" +msgstr "Non de-ripetuto" #: lib/pleroma/web/common_api/common_api.ex:428 #: lib/pleroma/web/common_api/common_api.ex:437 #, elixir-format msgid "Could not update state" -msgstr "" +msgstr "Non aggiornato" #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 #, elixir-format msgid "Error." -msgstr "" +msgstr "Errore." #: lib/pleroma/web/twitter_api/twitter_api.ex:106 #, elixir-format msgid "Invalid CAPTCHA" -msgstr "" +msgstr "CAPTCHA invalido" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 #: lib/pleroma/web/oauth/oauth_controller.ex:569 #, elixir-format msgid "Invalid credentials" -msgstr "" +msgstr "Credenziali invalide" #: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 #, elixir-format msgid "Invalid credentials." -msgstr "" +msgstr "Credenziali invalide." #: lib/pleroma/web/common_api/common_api.ex:265 #, elixir-format msgid "Invalid indices" -msgstr "" +msgstr "Indici invalidi" #: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 #, elixir-format msgid "Invalid parameters" -msgstr "" +msgstr "Parametri invalidi" #: lib/pleroma/web/common_api/utils.ex:411 #, elixir-format msgid "Invalid password." -msgstr "" +msgstr "Parola d'ordine invalida." #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 #, elixir-format msgid "Invalid request" -msgstr "" +msgstr "Richiesta invalida" #: lib/pleroma/web/twitter_api/twitter_api.ex:109 #, elixir-format msgid "Kocaptcha service unavailable" -msgstr "" +msgstr "Servizio Kocaptcha non disponibile" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 #, elixir-format msgid "Missing parameters" -msgstr "" +msgstr "Parametri mancanti" #: lib/pleroma/web/common_api/utils.ex:540 #, elixir-format msgid "No such conversation" -msgstr "" +msgstr "Conversazione inesistente" #: lib/pleroma/web/admin_api/admin_api_controller.ex:439 #: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 #, elixir-format msgid "No such permission_group" -msgstr "" +msgstr "permission_group non esistente" #: lib/pleroma/plugs/uploaded_media.ex:74 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135 #: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143 #, elixir-format msgid "Not found" -msgstr "" +msgstr "Non trovato" #: lib/pleroma/web/common_api/common_api.ex:241 #, elixir-format msgid "Poll's author can't vote" -msgstr "" +msgstr "L'autore del sondaggio non può votare" #: 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 @@ -288,215 +288,215 @@ msgstr "" #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 #, elixir-format msgid "Record not found" -msgstr "" +msgstr "Voce non trovata" #: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 #: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32 #: lib/pleroma/web/ostatus/ostatus_controller.ex:149 #, elixir-format msgid "Something went wrong" -msgstr "" +msgstr "C'è stato un problema" #: lib/pleroma/web/common_api/activity_draft.ex:107 #, elixir-format msgid "The message visibility must be direct" -msgstr "" +msgstr "Il messaggio dev'essere privato" #: lib/pleroma/web/common_api/utils.ex:566 #, elixir-format msgid "The status is over the character limit" -msgstr "" +msgstr "Il messaggio ha superato la lunghezza massima" #: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 #, elixir-format msgid "This resource requires authentication." -msgstr "" +msgstr "Accedi per leggere." #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 #, elixir-format msgid "Throttled" -msgstr "" +msgstr "Strozzato" #: lib/pleroma/web/common_api/common_api.ex:266 #, elixir-format msgid "Too many choices" -msgstr "" +msgstr "Troppe alternative" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 #, elixir-format msgid "Unhandled activity type" -msgstr "" +msgstr "Tipo di attività non gestibile" #: lib/pleroma/web/admin_api/admin_api_controller.ex:536 #, elixir-format msgid "You can't revoke your own admin status." -msgstr "" +msgstr "Non puoi divestirti da solo." #: lib/pleroma/web/oauth/oauth_controller.ex:218 #: lib/pleroma/web/oauth/oauth_controller.ex:309 #, elixir-format msgid "Your account is currently disabled" -msgstr "" +msgstr "Il tuo profilo è attualmente disabilitato" #: lib/pleroma/web/oauth/oauth_controller.ex:180 #: lib/pleroma/web/oauth/oauth_controller.ex:332 #, elixir-format msgid "Your login is missing a confirmed e-mail address" -msgstr "" +msgstr "Devi aggiungere un indirizzo email valido" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389 #, elixir-format msgid "can't read inbox of %{nickname} as %{as_nickname}" -msgstr "" +msgstr "non puoi leggere i messaggi privati di %{nickname} come %{as_nickname}" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472 #, elixir-format msgid "can't update outbox of %{nickname} as %{as_nickname}" -msgstr "" +msgstr "non puoi aggiornare gli inviati di %{nickname} come %{as_nickname}" #: lib/pleroma/web/common_api/common_api.ex:388 #, elixir-format msgid "conversation is already muted" -msgstr "" +msgstr "la conversazione è già zittita" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 #, elixir-format msgid "error" -msgstr "" +msgstr "errore" #: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 #, elixir-format msgid "mascots can only be images" -msgstr "" +msgstr "le mascotte possono solo essere immagini" #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 #, elixir-format msgid "not found" -msgstr "" +msgstr "non trovato" #: lib/pleroma/web/oauth/oauth_controller.ex:395 #, elixir-format msgid "Bad OAuth request." -msgstr "" +msgstr "Richiesta OAuth malformata." #: lib/pleroma/web/twitter_api/twitter_api.ex:115 #, elixir-format msgid "CAPTCHA already used" -msgstr "" +msgstr "CAPTCHA già utilizzato" #: lib/pleroma/web/twitter_api/twitter_api.ex:112 #, elixir-format msgid "CAPTCHA expired" -msgstr "" +msgstr "CAPTCHA scaduto" #: lib/pleroma/plugs/uploaded_media.ex:55 #, elixir-format msgid "Failed" -msgstr "" +msgstr "Fallito" #: lib/pleroma/web/oauth/oauth_controller.ex:411 #, elixir-format msgid "Failed to authenticate: %{message}." -msgstr "" +msgstr "Autenticazione fallita per: %{message}." #: lib/pleroma/web/oauth/oauth_controller.ex:442 #, elixir-format msgid "Failed to set up user account." -msgstr "" +msgstr "Profilo utente non creato." #: lib/pleroma/plugs/oauth_scopes_plug.ex:38 #, elixir-format msgid "Insufficient permissions: %{permissions}." -msgstr "" +msgstr "Permessi insufficienti: %{permissions}." #: lib/pleroma/plugs/uploaded_media.ex:94 #, elixir-format msgid "Internal Error" -msgstr "" +msgstr "Errore interno" #: lib/pleroma/web/oauth/fallback_controller.ex:22 #: lib/pleroma/web/oauth/fallback_controller.ex:29 #, elixir-format msgid "Invalid Username/Password" -msgstr "" +msgstr "Nome utente/parola d'ordine invalidi" #: lib/pleroma/web/twitter_api/twitter_api.ex:118 #, elixir-format msgid "Invalid answer data" -msgstr "" +msgstr "Risposta malformata" #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 #, elixir-format msgid "Nodeinfo schema version not handled" -msgstr "" +msgstr "Versione schema nodeinfo non compatibile" #: lib/pleroma/web/oauth/oauth_controller.ex:169 #, elixir-format msgid "This action is outside the authorized scopes" -msgstr "" +msgstr "Quest'azione non è consentita in questa visibilità" #: lib/pleroma/web/oauth/fallback_controller.ex:14 #, elixir-format msgid "Unknown error, please check the details and try again." -msgstr "" +msgstr "Errore sconosciuto, controlla i dettagli e riprova." #: lib/pleroma/web/oauth/oauth_controller.ex:116 #: lib/pleroma/web/oauth/oauth_controller.ex:155 #, elixir-format msgid "Unlisted redirect_uri." -msgstr "" +msgstr "redirect_uri nascosto." #: lib/pleroma/web/oauth/oauth_controller.ex:391 #, elixir-format msgid "Unsupported OAuth provider: %{provider}." -msgstr "" +msgstr "Gestore OAuth non supportato: %{provider}." #: lib/pleroma/uploaders/uploader.ex:72 #, elixir-format msgid "Uploader callback timeout" -msgstr "" +msgstr "Callback caricatmento scaduta" #: lib/pleroma/web/uploader_controller.ex:23 #, elixir-format msgid "bad request" -msgstr "" +msgstr "richiesta malformata" #: lib/pleroma/web/twitter_api/twitter_api.ex:103 #, elixir-format msgid "CAPTCHA Error" -msgstr "" +msgstr "Errore CAPTCHA" #: lib/pleroma/web/common_api/common_api.ex:200 #, elixir-format msgid "Could not add reaction emoji" -msgstr "" +msgstr "Reazione emoji non riuscita" #: lib/pleroma/web/common_api/common_api.ex:211 #, elixir-format msgid "Could not remove reaction emoji" -msgstr "" +msgstr "Rimozione reazione non riuscita" #: lib/pleroma/web/twitter_api/twitter_api.ex:129 #, elixir-format msgid "Invalid CAPTCHA (Missing parameter: %{name})" -msgstr "" +msgstr "CAPTCHA invalido (Parametro mancante: %{name})" #: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 #, elixir-format msgid "List not found" -msgstr "" +msgstr "Lista non trovata" #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 #, elixir-format msgid "Missing parameter: %{name}" -msgstr "" +msgstr "Parametro mancante: %{name}" #: lib/pleroma/web/oauth/oauth_controller.ex:207 #: lib/pleroma/web/oauth/oauth_controller.ex:322 #, elixir-format msgid "Password reset is required" -msgstr "" +msgstr "Necessario reimpostare parola d'ordine" #: lib/pleroma/tests/auth_test_controller.ex:9 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6 @@ -528,53 +528,58 @@ msgstr "" #, elixir-format msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." msgstr "" +"Sicurezza violata: il controllo autorizzazioni di OAuth non è stato svolto " +"né saltato." #: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 #, elixir-format msgid "Two-factor authentication enabled, you must use a access token." msgstr "" +"Autenticazione bifattoriale abilitata, devi utilizzare una chiave d'accesso." #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 #, elixir-format msgid "Unexpected error occurred while adding file to pack." -msgstr "" +msgstr "Errore inaspettato durante l'aggiunta del file al pacchetto." #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138 #, elixir-format msgid "Unexpected error occurred while creating pack." -msgstr "" +msgstr "Errore inaspettato durante la creazione del pacchetto." #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278 #, elixir-format msgid "Unexpected error occurred while removing file from pack." -msgstr "" +msgstr "Errore inaspettato durante la rimozione del file dal pacchetto." #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250 #, elixir-format msgid "Unexpected error occurred while updating file in pack." -msgstr "" +msgstr "Errore inaspettato durante l'aggiornamento del file nel pacchetto." #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179 #, elixir-format msgid "Unexpected error occurred while updating pack metadata." -msgstr "" +msgstr "Errore inaspettato durante l'aggiornamento dei metadati del pacchetto." -#: lib/pleroma/plugs/user_is_admin_plug.ex:40 +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 #, elixir-format -msgid "User is not an admin or OAuth admin scope is not granted." +msgid "User is not an admin." msgstr "" +"L'utente non è un amministratore." +"OAuth." #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #, elixir-format msgid "Web push subscription is disabled on this Pleroma instance" -msgstr "" +msgstr "Gli aggiornamenti web push non sono disponibili in questa stanza" #: lib/pleroma/web/admin_api/admin_api_controller.ex:502 #, elixir-format msgid "You can't revoke your own admin/moderator status." -msgstr "" +msgstr "Non puoi divestire te stesso." #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105 #, elixir-format msgid "authorization required for timeline view" -msgstr "" +msgstr "autorizzazione richiesta per vedere la sequenza" diff --git a/priv/gettext/nl/LC_MESSAGES/errors.po b/priv/gettext/nl/LC_MESSAGES/errors.po index 3118f6b5d..cfcb05fe6 100644 --- a/priv/gettext/nl/LC_MESSAGES/errors.po +++ b/priv/gettext/nl/LC_MESSAGES/errors.po @@ -559,9 +559,9 @@ msgstr "" msgid "Unexpected error occurred while updating pack metadata." msgstr "" -#: lib/pleroma/plugs/user_is_admin_plug.ex:40 +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 #, elixir-format -msgid "User is not an admin or OAuth admin scope is not granted." +msgid "User is not an admin." msgstr "" #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 diff --git a/priv/gettext/pl/LC_MESSAGES/errors.po b/priv/gettext/pl/LC_MESSAGES/errors.po index 7bc39c52a..653ea00a1 100644 --- a/priv/gettext/pl/LC_MESSAGES/errors.po +++ b/priv/gettext/pl/LC_MESSAGES/errors.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-05-13 16:37+0000\n" -"PO-Revision-Date: 2020-05-16 17:13+0000\n" -"Last-Translator: Jędrzej Tomaszewski \n" +"PO-Revision-Date: 2020-07-09 14:40+0000\n" +"Last-Translator: Ben Is \n" "Language-Team: Polish \n" "Language: pl\n" @@ -50,7 +50,7 @@ msgstr "jest zarezerwowany" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "nie pasuje do potwierdzenia" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" @@ -566,9 +566,9 @@ msgstr "Nieoczekiwany błąd podczas zmieniania pliku w paczce." msgid "Unexpected error occurred while updating pack metadata." msgstr "Nieoczekiwany błąd podczas zmieniania metadanych paczki." -#: lib/pleroma/plugs/user_is_admin_plug.ex:40 +#: lib/pleroma/plugs/user_is_admin_plug.ex:21 #, elixir-format -msgid "User is not an admin or OAuth admin scope is not granted." +msgid "User is not an admin." msgstr "" #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 diff --git a/priv/repo/migrations/20200626163359_rename_notification_privacy_option.exs b/priv/repo/migrations/20200626163359_rename_notification_privacy_option.exs new file mode 100644 index 000000000..06d7f7272 --- /dev/null +++ b/priv/repo/migrations/20200626163359_rename_notification_privacy_option.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.RenameNotificationPrivacyOption do + use Ecto.Migration + + def up do + execute( + "UPDATE users SET notification_settings = notification_settings - 'privacy_option' || jsonb_build_object('hide_notification_contents', notification_settings->'privacy_option') +where notification_settings ? 'privacy_option' +and local" + ) + end + + def down do + execute( + "UPDATE users SET notification_settings = notification_settings - 'hide_notification_contents' || jsonb_build_object('privacy_option', notification_settings->'hide_notification_contents') +where notification_settings ? 'hide_notification_contents' +and local" + ) + end +end diff --git a/priv/repo/migrations/20200630162024_rename_user_settings_col.exs b/priv/repo/migrations/20200630162024_rename_user_settings_col.exs new file mode 100644 index 000000000..2355eb681 --- /dev/null +++ b/priv/repo/migrations/20200630162024_rename_user_settings_col.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.RenameUserSettingsCol do + use Ecto.Migration + + def up do + rename(table(:users), :settings, to: :mastofe_settings) + end + + def down do + rename(table(:users), :mastofe_settings, to: :settings) + end +end diff --git a/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs new file mode 100644 index 000000000..8dfda89f1 --- /dev/null +++ b/priv/repo/migrations/20200703101031_add_chat_acceptance_to_users.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.AddChatAcceptanceToUsers do + use Ecto.Migration + + def up do + alter table(:users) do + add(:accepts_chat_messages, :boolean, nullable: true) + end + + execute("update users set accepts_chat_messages = true where local = true") + end + + def down do + alter table(:users) do + remove(:accepts_chat_messages) + end + end +end diff --git a/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs b/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs new file mode 100644 index 000000000..798687f8a --- /dev/null +++ b/priv/repo/migrations/20200706060258_remove_tesla_from_config.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.RemoveTeslaFromConfig do + use Ecto.Migration + + def up do + execute("DELETE FROM config WHERE config.group = ':tesla'") + end + + def down do + end +end diff --git a/priv/repo/migrations/20200707112859_instances_add_favicon.exs b/priv/repo/migrations/20200707112859_instances_add_favicon.exs new file mode 100644 index 000000000..5538749dc --- /dev/null +++ b/priv/repo/migrations/20200707112859_instances_add_favicon.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.InstancesAddFavicon do + use Ecto.Migration + + def change do + alter table(:instances) do + add(:favicon, :string) + add(:favicon_updated_at, :naive_datetime) + end + end +end diff --git a/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs b/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs new file mode 100644 index 000000000..94efe323a --- /dev/null +++ b/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Repo.Migrations.DropUserTrigramIndex do + @moduledoc "Drops unused trigram index on `users` (FTS index is being used instead)" + + use Ecto.Migration + + def up do + drop_if_exists(index(:users, [], name: :users_trigram_index)) + end + + def down do + create_if_not_exists( + index(:users, ["(trim(nickname || ' ' || coalesce(name, ''))) gist_trgm_ops"], + name: :users_trigram_index, + using: :gist + ) + ) + end +end diff --git a/priv/repo/migrations/20200714081657_oban_2_0_config_changes.exs b/priv/repo/migrations/20200714081657_oban_2_0_config_changes.exs new file mode 100644 index 000000000..c54bb2511 --- /dev/null +++ b/priv/repo/migrations/20200714081657_oban_2_0_config_changes.exs @@ -0,0 +1,27 @@ +defmodule Elixir.Pleroma.Repo.Migrations.Oban20ConfigChanges do + use Ecto.Migration + import Ecto.Query + alias Pleroma.ConfigDB + alias Pleroma.Repo + + def change do + config_entry = + from(c in ConfigDB, where: c.group == ^":pleroma" and c.key == ^"Oban") + |> select([c], struct(c, [:value, :id])) + |> Repo.one() + + if config_entry do + %{value: value} = config_entry + + value = + case Keyword.fetch(value, :verbose) do + {:ok, log} -> Keyword.put_new(value, :log, log) + _ -> value + end + |> Keyword.drop([:verbose, :prune]) + + Ecto.Changeset.change(config_entry, %{value: value}) + |> Repo.update() + end + end +end diff --git a/priv/static/adminfe/app.796ca6d4.css b/priv/static/adminfe/app.01bdb34a.css similarity index 100% rename from priv/static/adminfe/app.796ca6d4.css rename to priv/static/adminfe/app.01bdb34a.css diff --git a/priv/static/adminfe/chunk-0558.af0d89cd.css b/priv/static/adminfe/chunk-070d.d2dd6533.css similarity index 100% rename from priv/static/adminfe/chunk-0558.af0d89cd.css rename to priv/static/adminfe/chunk-070d.d2dd6533.css diff --git a/priv/static/adminfe/chunk-0cbc.60bba79b.css b/priv/static/adminfe/chunk-0cbc.60bba79b.css new file mode 100644 index 000000000..c6280f7ef Binary files /dev/null and b/priv/static/adminfe/chunk-0cbc.60bba79b.css differ diff --git a/priv/static/adminfe/chunk-6b68.0cc00484.css b/priv/static/adminfe/chunk-143c.43ada4fc.css similarity index 53% rename from priv/static/adminfe/chunk-6b68.0cc00484.css rename to priv/static/adminfe/chunk-143c.43ada4fc.css index 7061b3d03..b580e0699 100644 Binary files a/priv/static/adminfe/chunk-6b68.0cc00484.css and b/priv/static/adminfe/chunk-143c.43ada4fc.css differ diff --git a/priv/static/adminfe/chunk-1609.408dae86.css b/priv/static/adminfe/chunk-1609.408dae86.css new file mode 100644 index 000000000..483d88545 Binary files /dev/null and b/priv/static/adminfe/chunk-1609.408dae86.css differ diff --git a/priv/static/adminfe/chunk-176e.5d7d957b.css b/priv/static/adminfe/chunk-176e.5d7d957b.css new file mode 100644 index 000000000..0bedf3773 Binary files /dev/null and b/priv/static/adminfe/chunk-176e.5d7d957b.css differ diff --git a/priv/static/adminfe/chunk-22d2.813009b9.css b/priv/static/adminfe/chunk-22d2.813009b9.css deleted file mode 100644 index f0a98583e..000000000 Binary files a/priv/static/adminfe/chunk-22d2.813009b9.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-3384.d50ed383.css b/priv/static/adminfe/chunk-3384.d50ed383.css deleted file mode 100644 index 70ae2a26b..000000000 Binary files a/priv/static/adminfe/chunk-3384.d50ed383.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-43ca.af749c6c.css b/priv/static/adminfe/chunk-43ca.af749c6c.css new file mode 100644 index 000000000..504affb93 Binary files /dev/null and b/priv/static/adminfe/chunk-43ca.af749c6c.css differ diff --git a/priv/static/adminfe/chunk-0961.d3692214.css b/priv/static/adminfe/chunk-4e7e.5afe1978.css similarity index 100% rename from priv/static/adminfe/chunk-0961.d3692214.css rename to priv/static/adminfe/chunk-4e7e.5afe1978.css diff --git a/priv/static/adminfe/chunk-5882.f65db7f2.css b/priv/static/adminfe/chunk-5882.f65db7f2.css new file mode 100644 index 000000000..b5e2a00b0 Binary files /dev/null and b/priv/static/adminfe/chunk-5882.f65db7f2.css differ diff --git a/priv/static/adminfe/chunk-6e81.0e80d020.css b/priv/static/adminfe/chunk-6e81.ca3b222f.css similarity index 100% rename from priv/static/adminfe/chunk-6e81.0e80d020.css rename to priv/static/adminfe/chunk-6e81.ca3b222f.css diff --git a/priv/static/adminfe/chunk-7506.f01f6c2a.css b/priv/static/adminfe/chunk-7506.f01f6c2a.css new file mode 100644 index 000000000..93d3eac84 Binary files /dev/null and b/priv/static/adminfe/chunk-7506.f01f6c2a.css differ diff --git a/priv/static/adminfe/chunk-7637.941c4edb.css b/priv/static/adminfe/chunk-7637.941c4edb.css deleted file mode 100644 index be1d183a9..000000000 Binary files a/priv/static/adminfe/chunk-7637.941c4edb.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-0778.d9e7180a.css b/priv/static/adminfe/chunk-7c6b.d9e7180a.css similarity index 100% rename from priv/static/adminfe/chunk-0778.d9e7180a.css rename to priv/static/adminfe/chunk-7c6b.d9e7180a.css diff --git a/priv/static/adminfe/chunk-7e30.f2b9674a.css b/priv/static/adminfe/chunk-7e30.f2b9674a.css deleted file mode 100644 index a4a56712e..000000000 Binary files a/priv/static/adminfe/chunk-7e30.f2b9674a.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-970d.f59cca8c.css b/priv/static/adminfe/chunk-970d.f59cca8c.css deleted file mode 100644 index 15511f12f..000000000 Binary files a/priv/static/adminfe/chunk-970d.f59cca8c.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-c5f4.b1112f18.css b/priv/static/adminfe/chunk-c5f4.b1112f18.css new file mode 100644 index 000000000..d3b7604aa Binary files /dev/null and b/priv/static/adminfe/chunk-c5f4.b1112f18.css differ diff --git a/priv/static/adminfe/chunk-commons.7f6d2d11.css b/priv/static/adminfe/chunk-commons.7f6d2d11.css new file mode 100644 index 000000000..42f5e0ee9 Binary files /dev/null and b/priv/static/adminfe/chunk-commons.7f6d2d11.css differ diff --git a/priv/static/adminfe/chunk-d38a.cabdc22e.css b/priv/static/adminfe/chunk-d38a.cabdc22e.css deleted file mode 100644 index 4a2bf472b..000000000 Binary files a/priv/static/adminfe/chunk-d38a.cabdc22e.css and /dev/null differ diff --git a/priv/static/adminfe/chunk-e404.a56021ae.css b/priv/static/adminfe/chunk-e404.a56021ae.css new file mode 100644 index 000000000..7d8596ef6 Binary files /dev/null and b/priv/static/adminfe/chunk-e404.a56021ae.css differ diff --git a/priv/static/adminfe/chunk-e458.6c0703cb.css b/priv/static/adminfe/chunk-e458.6c0703cb.css deleted file mode 100644 index 6d2a5d996..000000000 Binary files a/priv/static/adminfe/chunk-e458.6c0703cb.css and /dev/null differ diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html index 73e680115..22b3143d2 100644 --- a/priv/static/adminfe/index.html +++ b/priv/static/adminfe/index.html @@ -1 +1 @@ -Admin FE

\ No newline at end of file +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/js/app.0146039c.js b/priv/static/adminfe/static/js/app.0146039c.js deleted file mode 100644 index ab08475ad..000000000 Binary files a/priv/static/adminfe/static/js/app.0146039c.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/app.0146039c.js.map b/priv/static/adminfe/static/js/app.0146039c.js.map deleted file mode 100644 index 178715dc6..000000000 Binary files a/priv/static/adminfe/static/js/app.0146039c.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/app.f220ac13.js b/priv/static/adminfe/static/js/app.f220ac13.js new file mode 100644 index 000000000..e5e1eda91 Binary files /dev/null and b/priv/static/adminfe/static/js/app.f220ac13.js differ diff --git a/priv/static/adminfe/static/js/app.f220ac13.js.map b/priv/static/adminfe/static/js/app.f220ac13.js.map new file mode 100644 index 000000000..90c22121e Binary files /dev/null and b/priv/static/adminfe/static/js/app.f220ac13.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-0558.75954137.js b/priv/static/adminfe/static/js/chunk-070d.7e10a520.js similarity index 98% rename from priv/static/adminfe/static/js/chunk-0558.75954137.js rename to priv/static/adminfe/static/js/chunk-070d.7e10a520.js index 7b29707fa..8726dbcd3 100644 Binary files a/priv/static/adminfe/static/js/chunk-0558.75954137.js and b/priv/static/adminfe/static/js/chunk-070d.7e10a520.js differ diff --git a/priv/static/adminfe/static/js/chunk-0558.75954137.js.map b/priv/static/adminfe/static/js/chunk-070d.7e10a520.js.map similarity index 99% rename from priv/static/adminfe/static/js/chunk-0558.75954137.js.map rename to priv/static/adminfe/static/js/chunk-070d.7e10a520.js.map index e9e2affb6..6b75a215e 100644 Binary files a/priv/static/adminfe/static/js/chunk-0558.75954137.js.map and b/priv/static/adminfe/static/js/chunk-070d.7e10a520.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map b/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map deleted file mode 100644 index 1f96c3236..000000000 Binary files a/priv/static/adminfe/static/js/chunk-0778.b17650df.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js b/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js new file mode 100644 index 000000000..d29070b62 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js differ diff --git a/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js.map b/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js.map new file mode 100644 index 000000000..7c99d9d48 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-0cbc.2b0f8802.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js b/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js new file mode 100644 index 000000000..6fbc5b1ed Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js differ diff --git a/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js.map b/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js.map new file mode 100644 index 000000000..425a7427a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-143c.fc1825bf.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-1609.98da6b01.js b/priv/static/adminfe/static/js/chunk-1609.98da6b01.js new file mode 100644 index 000000000..29dbad261 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-1609.98da6b01.js differ diff --git a/priv/static/adminfe/static/js/chunk-1609.98da6b01.js.map b/priv/static/adminfe/static/js/chunk-1609.98da6b01.js.map new file mode 100644 index 000000000..f287a503a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-1609.98da6b01.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-176e.c4995511.js b/priv/static/adminfe/static/js/chunk-176e.c4995511.js new file mode 100644 index 000000000..80474b904 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-176e.c4995511.js differ diff --git a/priv/static/adminfe/static/js/chunk-176e.c4995511.js.map b/priv/static/adminfe/static/js/chunk-176e.c4995511.js.map new file mode 100644 index 000000000..f0caa5f62 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-176e.c4995511.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js deleted file mode 100644 index 903f553b0..000000000 Binary files a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map b/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map deleted file mode 100644 index 68735ed26..000000000 Binary files a/priv/static/adminfe/static/js/chunk-22d2.a0cf7976.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js b/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js deleted file mode 100644 index 6a161a0c6..000000000 Binary files a/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js.map b/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js.map deleted file mode 100644 index b08db9d6e..000000000 Binary files a/priv/static/adminfe/static/js/chunk-3384.b2ebeeca.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js b/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js new file mode 100644 index 000000000..f9fcbc288 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js differ diff --git a/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js.map b/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js.map new file mode 100644 index 000000000..3c71ad178 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-43ca.aceb457c.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js b/priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js similarity index 97% rename from priv/static/adminfe/static/js/chunk-0961.ef33e81b.js rename to priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js index e090bb93c..0fdf0de50 100644 Binary files a/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js and b/priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js differ diff --git a/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map b/priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js.map similarity index 99% rename from priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map rename to priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js.map index 97c6a4b54..7a6751cf8 100644 Binary files a/priv/static/adminfe/static/js/chunk-0961.ef33e81b.js.map and b/priv/static/adminfe/static/js/chunk-4e7e.91b5e73a.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js similarity index 99% rename from priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js rename to priv/static/adminfe/static/js/chunk-5118.7c48ad58.js index 9fb60af23..2357e225d 100644 Binary files a/priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js and b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js differ diff --git a/priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js.map b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map similarity index 99% rename from priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js.map rename to priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map index 241c6cc21..c29b4b170 100644 Binary files a/priv/static/adminfe/static/js/chunk-7f9e.c49aa694.js.map and b/priv/static/adminfe/static/js/chunk-5118.7c48ad58.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js b/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js new file mode 100644 index 000000000..a29b6daab Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js differ diff --git a/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js.map b/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js.map new file mode 100644 index 000000000..d1aa2037f Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-5882.7cbc4c1b.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js deleted file mode 100644 index bfdf936f8..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map b/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map deleted file mode 100644 index d1d728b80..000000000 Binary files a/priv/static/adminfe/static/js/chunk-6b68.fbc0f684.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js b/priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js similarity index 97% rename from priv/static/adminfe/static/js/chunk-6e81.3733ace2.js rename to priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js index c888ce03f..f40d31879 100644 Binary files a/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js and b/priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js differ diff --git a/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map b/priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js.map similarity index 98% rename from priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map rename to priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js.map index 63128dd67..0390c3309 100644 Binary files a/priv/static/adminfe/static/js/chunk-6e81.3733ace2.js.map and b/priv/static/adminfe/static/js/chunk-6e81.6efb01f4.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-7506.a3364e53.js b/priv/static/adminfe/static/js/chunk-7506.a3364e53.js new file mode 100644 index 000000000..d4eaa356a Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7506.a3364e53.js differ diff --git a/priv/static/adminfe/static/js/chunk-7506.a3364e53.js.map b/priv/static/adminfe/static/js/chunk-7506.a3364e53.js.map new file mode 100644 index 000000000..c8e9db8e0 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7506.a3364e53.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js deleted file mode 100644 index b38644b98..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map b/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map deleted file mode 100644 index ddd53f1cd..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7637.8f5fb36e.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-0778.b17650df.js b/priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js similarity index 83% rename from priv/static/adminfe/static/js/chunk-0778.b17650df.js rename to priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js index 1a174cc1e..27478ddb1 100644 Binary files a/priv/static/adminfe/static/js/chunk-0778.b17650df.js and b/priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js differ diff --git a/priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js.map b/priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js.map new file mode 100644 index 000000000..2114a3c52 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-7c6b.e63ae1da.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js b/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js deleted file mode 100644 index 0a0e1ca34..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js.map b/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js.map deleted file mode 100644 index bc47158ea..000000000 Binary files a/priv/static/adminfe/static/js/chunk-7e30.ec42e302.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-970d.2457e066.js b/priv/static/adminfe/static/js/chunk-970d.2457e066.js deleted file mode 100644 index 0f99d835e..000000000 Binary files a/priv/static/adminfe/static/js/chunk-970d.2457e066.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map b/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map deleted file mode 100644 index 6896407b0..000000000 Binary files a/priv/static/adminfe/static/js/chunk-970d.2457e066.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js b/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js new file mode 100644 index 000000000..2d5308031 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js differ diff --git a/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js.map b/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js.map new file mode 100644 index 000000000..d5fc047ee Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-c5f4.cf269f9b.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-commons.5a106955.js b/priv/static/adminfe/static/js/chunk-commons.5a106955.js new file mode 100644 index 000000000..a6cf2ce52 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-commons.5a106955.js differ diff --git a/priv/static/adminfe/static/js/chunk-commons.5a106955.js.map b/priv/static/adminfe/static/js/chunk-commons.5a106955.js.map new file mode 100644 index 000000000..d924490e5 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-commons.5a106955.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js deleted file mode 100644 index c302af310..000000000 Binary files a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map b/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map deleted file mode 100644 index 6779f6dc1..000000000 Binary files a/priv/static/adminfe/static/js/chunk-d38a.a851004a.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js b/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js new file mode 100644 index 000000000..769e9f4f9 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js differ diff --git a/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js.map b/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js.map new file mode 100644 index 000000000..e8214adbb Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-e404.554bc2e3.js.map differ diff --git a/priv/static/adminfe/static/js/chunk-e458.bb460d81.js b/priv/static/adminfe/static/js/chunk-e458.bb460d81.js deleted file mode 100644 index a08717166..000000000 Binary files a/priv/static/adminfe/static/js/chunk-e458.bb460d81.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/chunk-e458.bb460d81.js.map b/priv/static/adminfe/static/js/chunk-e458.bb460d81.js.map deleted file mode 100644 index 89f05fb99..000000000 Binary files a/priv/static/adminfe/static/js/chunk-e458.bb460d81.js.map and /dev/null differ diff --git a/priv/static/adminfe/static/js/runtime.0a70a9f5.js b/priv/static/adminfe/static/js/runtime.0a70a9f5.js new file mode 100644 index 000000000..a99d1d369 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.0a70a9f5.js differ diff --git a/priv/static/adminfe/static/js/runtime.0a70a9f5.js.map b/priv/static/adminfe/static/js/runtime.0a70a9f5.js.map new file mode 100644 index 000000000..62e726b22 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.0a70a9f5.js.map differ diff --git a/priv/static/adminfe/static/js/runtime.b08eb412.js b/priv/static/adminfe/static/js/runtime.b08eb412.js deleted file mode 100644 index 2a9a4e0db..000000000 Binary files a/priv/static/adminfe/static/js/runtime.b08eb412.js and /dev/null differ diff --git a/priv/static/adminfe/static/js/runtime.b08eb412.js.map b/priv/static/adminfe/static/js/runtime.b08eb412.js.map deleted file mode 100644 index 62f70ee3e..000000000 Binary files a/priv/static/adminfe/static/js/runtime.b08eb412.js.map and /dev/null differ diff --git a/priv/static/index.html b/priv/static/index.html index ddd4ec4eb..2257dec35 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 7cc3fee40..e7722cf72 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -13,6 +13,7 @@ }, "discoverable": "toot:discoverable", "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "capabilities": "litepub:capabilities", "ostatus": "http://ostatus.org#", "schema": "http://schema.org#", "toot": "http://joinmastodon.org/ns#", diff --git a/priv/static/static-fe/static-fe.css b/priv/static/static-fe/static-fe.css index db61ff266..89e9f4877 100644 Binary files a/priv/static/static-fe/static-fe.css and b/priv/static/static-fe/static-fe.css differ diff --git a/priv/static/static/config.json b/priv/static/static/config.json index 727dde73b..0030f78f1 100644 --- a/priv/static/static/config.json +++ b/priv/static/static/config.json @@ -14,7 +14,6 @@ "logoMargin": ".1em", "logoMask": true, "minimalScopesMode": false, - "noAttachmentLinks": false, "nsfwCensorImage": "", "postContentType": "text/plain", "redirectRootLogin": "/main/friends", @@ -22,6 +21,7 @@ "scopeCopy": true, "showFeaturesPanel": true, "showInstanceSpecificPanel": false, + "sidebarRight": false, "subjectLineBehavior": "email", "theme": "pleroma-dark", "webPushNotifications": false diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css b/priv/static/static/css/2.0778a6a864a1307a6c41.css new file mode 100644 index 000000000..a33585ef1 Binary files /dev/null and b/priv/static/static/css/2.0778a6a864a1307a6c41.css differ diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css.map b/priv/static/static/css/2.0778a6a864a1307a6c41.css.map new file mode 100644 index 000000000..28cd8ba54 --- /dev/null +++ b/priv/static/static/css/2.0778a6a864a1307a6c41.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/2.0778a6a864a1307a6c41.css","sourcesContent":[".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css b/priv/static/static/css/3.b2603a50868c68a1c192.css similarity index 92% rename from priv/static/static/css/vendors~app.18fea621d430000acc27.css rename to priv/static/static/css/3.b2603a50868c68a1c192.css index ef783cbb3..4cec5785b 100644 Binary files a/priv/static/static/css/vendors~app.18fea621d430000acc27.css and b/priv/static/static/css/3.b2603a50868c68a1c192.css differ diff --git a/priv/static/static/css/3.b2603a50868c68a1c192.css.map b/priv/static/static/css/3.b2603a50868c68a1c192.css.map new file mode 100644 index 000000000..805e7dc04 --- /dev/null +++ b/priv/static/static/css/3.b2603a50868c68a1c192.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/3.b2603a50868c68a1c192.css","sourcesContent":["/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css b/priv/static/static/css/app.613cef07981cd95ccceb.css deleted file mode 100644 index c1d5f8188..000000000 Binary files a/priv/static/static/css/app.613cef07981cd95ccceb.css and /dev/null differ diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css.map b/priv/static/static/css/app.613cef07981cd95ccceb.css.map deleted file mode 100644 index 556e0bb0b..000000000 --- a/priv/static/static/css/app.613cef07981cd95ccceb.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA,uBAAuB,aAAa,kBAAkB,qBAAqB,sBAAsB,qCAAqC,8BAA8B,e;ACApK,cAAc,oBAAoB,aAAa,0BAA0B,sBAAsB,wBAAwB,kBAAkB,cAAc,eAAe,gCAAgC,aAAa,wCAAwC,0BAA0B,aAAa,gBAAgB,oBAAoB,oBAAoB,aAAa,kBAAkB,WAAW,kBAAkB,gBAAgB,gBAAgB,sBAAsB,uDAAuD,cAAc,WAAW,kBAAkB,cAAc,wBAAwB,yBAAyB,wCAAwC,iCAAiC,YAAY,kBAAkB,oBAAoB,aAAa,kBAAkB,cAAc,sCAAsC,WAAW,cAAc,kBAAkB,4BAA4B,6BAA6B,gBAAgB,oBAAoB,oBAAoB,mBAAmB,cAAc,8BAA8B,yBAAyB,qCAAqC,mDAAmD,UAAU,yDAAyD,UAAU,6CAA6C,uBAAuB,UAAU,cAAc,oCAAoC,0CAA0C,gBAAgB,mBAAmB,gBAAgB,qDAAqD,WAAW,kBAAkB,OAAO,QAAQ,SAAS,UAAU,wBAAwB,yBAAyB,wC;ACAtlD,2BAA2B,aAAa,kBAAkB,kCAAkC,e","file":"static/css/app.613cef07981cd95ccceb.css","sourcesContent":[".with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}",".tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:\"\";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:\"\";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}",".with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.6dbc7dea4fc148c85860.css b/priv/static/static/css/app.6dbc7dea4fc148c85860.css new file mode 100644 index 000000000..3927e3b77 Binary files /dev/null and b/priv/static/static/css/app.6dbc7dea4fc148c85860.css differ diff --git a/priv/static/static/css/app.6dbc7dea4fc148c85860.css.map b/priv/static/static/css/app.6dbc7dea4fc148c85860.css.map new file mode 100644 index 000000000..963d5b3b8 --- /dev/null +++ b/priv/static/static/css/app.6dbc7dea4fc148c85860.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.6dbc7dea4fc148c85860.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n font-size: 2em;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 1em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map b/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map deleted file mode 100644 index 057d67d6a..000000000 --- a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/vendors~app.18fea621d430000acc27.css","sourcesContent":["/*!\n * Cropper.js v1.5.6\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2019-10-04T04:33:44.164Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/fontello.1589385935077.woff b/priv/static/static/font/fontello.1589385935077.woff deleted file mode 100644 index f48488a77..000000000 Binary files a/priv/static/static/font/fontello.1589385935077.woff and /dev/null differ diff --git a/priv/static/static/font/fontello.1589385935077.woff2 b/priv/static/static/font/fontello.1589385935077.woff2 deleted file mode 100644 index 012eb9305..000000000 Binary files a/priv/static/static/font/fontello.1589385935077.woff2 and /dev/null differ diff --git a/priv/static/static/font/fontello.1589385935077.eot b/priv/static/static/font/fontello.1594823398494.eot similarity index 80% rename from priv/static/static/font/fontello.1589385935077.eot rename to priv/static/static/font/fontello.1594823398494.eot index e5f37013a..12e6beabf 100644 Binary files a/priv/static/static/font/fontello.1589385935077.eot and b/priv/static/static/font/fontello.1594823398494.eot differ diff --git a/priv/static/static/font/fontello.1589385935077.svg b/priv/static/static/font/fontello.1594823398494.svg similarity index 92% rename from priv/static/static/font/fontello.1589385935077.svg rename to priv/static/static/font/fontello.1594823398494.svg index e63fb7529..71b5d70af 100644 --- a/priv/static/static/font/fontello.1589385935077.svg +++ b/priv/static/static/font/fontello.1594823398494.svg @@ -80,8 +80,18 @@ + + + + + + + + + + @@ -90,6 +100,10 @@ + + + + diff --git a/priv/static/static/font/fontello.1589385935077.ttf b/priv/static/static/font/fontello.1594823398494.ttf similarity index 80% rename from priv/static/static/font/fontello.1589385935077.ttf rename to priv/static/static/font/fontello.1594823398494.ttf index 0fde96cea..6f21845a8 100644 Binary files a/priv/static/static/font/fontello.1589385935077.ttf and b/priv/static/static/font/fontello.1594823398494.ttf differ diff --git a/priv/static/static/font/fontello.1594823398494.woff b/priv/static/static/font/fontello.1594823398494.woff new file mode 100644 index 000000000..a7cd098f4 Binary files /dev/null and b/priv/static/static/font/fontello.1594823398494.woff differ diff --git a/priv/static/static/font/fontello.1594823398494.woff2 b/priv/static/static/font/fontello.1594823398494.woff2 new file mode 100644 index 000000000..c61bf111a Binary files /dev/null and b/priv/static/static/font/fontello.1594823398494.woff2 differ diff --git a/priv/static/static/fontello.1589385935077.css b/priv/static/static/fontello.1594823398494.css similarity index 82% rename from priv/static/static/fontello.1589385935077.css rename to priv/static/static/fontello.1594823398494.css index 746492163..fe61b94c6 100644 Binary files a/priv/static/static/fontello.1589385935077.css and b/priv/static/static/fontello.1594823398494.css differ diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json old mode 100755 new mode 100644 index 7f0e7cdd5..706800cdb --- a/priv/static/static/fontello.json +++ b/priv/static/static/fontello.json @@ -363,6 +363,48 @@ "css": "ok", "code": 59431, "src": "fontawesome" + }, + { + "uid": "4109c474ff99cad28fd5a2c38af2ec6f", + "css": "filter", + "code": 61616, + "src": "fontawesome" + }, + { + "uid": "9a76bc135eac17d2c8b8ad4a5774fc87", + "css": "download", + "code": 59429, + "src": "fontawesome" + }, + { + "uid": "f04a5d24e9e659145b966739c4fde82a", + "css": "bookmark", + "code": 59430, + "src": "fontawesome" + }, + { + "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", + "css": "bookmark-empty", + "code": 61591, + "src": "fontawesome" + }, + { + "uid": "9ea0a737ccc45d6c510dcbae56058849", + "css": "music", + "code": 59432, + "src": "fontawesome" + }, + { + "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", + "css": "doc", + "code": 59433, + "src": "fontawesome" + }, + { + "uid": "98d9c83c1ee7c2c25af784b518c522c5", + "css": "block", + "code": 59434, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/priv/static/static/js/10.5ef4671883649cf93524.js b/priv/static/static/js/10.5ef4671883649cf93524.js new file mode 100644 index 000000000..6819c854b Binary files /dev/null and b/priv/static/static/js/10.5ef4671883649cf93524.js differ diff --git a/priv/static/static/js/10.5ef4671883649cf93524.js.map b/priv/static/static/js/10.5ef4671883649cf93524.js.map new file mode 100644 index 000000000..95fa2207e Binary files /dev/null and b/priv/static/static/js/10.5ef4671883649cf93524.js.map differ diff --git a/priv/static/static/js/11.c5b938b4349f87567338.js b/priv/static/static/js/11.c5b938b4349f87567338.js new file mode 100644 index 000000000..b97f69bf3 Binary files /dev/null and b/priv/static/static/js/11.c5b938b4349f87567338.js differ diff --git a/priv/static/static/js/11.c5b938b4349f87567338.js.map b/priv/static/static/js/11.c5b938b4349f87567338.js.map new file mode 100644 index 000000000..5ccf83b1d Binary files /dev/null and b/priv/static/static/js/11.c5b938b4349f87567338.js.map differ diff --git a/priv/static/static/js/12.ab82f9512fa85e78c114.js b/priv/static/static/js/12.ab82f9512fa85e78c114.js new file mode 100644 index 000000000..100d72b33 Binary files /dev/null and b/priv/static/static/js/12.ab82f9512fa85e78c114.js differ diff --git a/priv/static/static/js/12.ab82f9512fa85e78c114.js.map b/priv/static/static/js/12.ab82f9512fa85e78c114.js.map new file mode 100644 index 000000000..23335ae23 Binary files /dev/null and b/priv/static/static/js/12.ab82f9512fa85e78c114.js.map differ diff --git a/priv/static/static/js/13.40e59c5015d3307b94ad.js b/priv/static/static/js/13.40e59c5015d3307b94ad.js new file mode 100644 index 000000000..2088bb6b7 Binary files /dev/null and b/priv/static/static/js/13.40e59c5015d3307b94ad.js differ diff --git a/priv/static/static/js/13.40e59c5015d3307b94ad.js.map b/priv/static/static/js/13.40e59c5015d3307b94ad.js.map new file mode 100644 index 000000000..3931b5ef9 Binary files /dev/null and b/priv/static/static/js/13.40e59c5015d3307b94ad.js.map differ diff --git a/priv/static/static/js/14.de791a47ee5249a526b1.js b/priv/static/static/js/14.de791a47ee5249a526b1.js new file mode 100644 index 000000000..0e341275e Binary files /dev/null and b/priv/static/static/js/14.de791a47ee5249a526b1.js differ diff --git a/priv/static/static/js/14.de791a47ee5249a526b1.js.map b/priv/static/static/js/14.de791a47ee5249a526b1.js.map new file mode 100644 index 000000000..4bef54546 Binary files /dev/null and b/priv/static/static/js/14.de791a47ee5249a526b1.js.map differ diff --git a/priv/static/static/js/15.e24854297ad682aec45a.js b/priv/static/static/js/15.e24854297ad682aec45a.js new file mode 100644 index 000000000..671370192 Binary files /dev/null and b/priv/static/static/js/15.e24854297ad682aec45a.js differ diff --git a/priv/static/static/js/15.e24854297ad682aec45a.js.map b/priv/static/static/js/15.e24854297ad682aec45a.js.map new file mode 100644 index 000000000..89789a542 Binary files /dev/null and b/priv/static/static/js/15.e24854297ad682aec45a.js.map differ diff --git a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js new file mode 100644 index 000000000..6a3ea9513 Binary files /dev/null and b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js differ diff --git a/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map new file mode 100644 index 000000000..fec45b087 Binary files /dev/null and b/priv/static/static/js/16.b7b0e4b8227a50fcb9bb.js.map differ diff --git a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js b/priv/static/static/js/17.c98118b6bb84ee3b5b08.js new file mode 100644 index 000000000..c41f0b6b8 Binary files /dev/null and b/priv/static/static/js/17.c98118b6bb84ee3b5b08.js differ diff --git a/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map b/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map new file mode 100644 index 000000000..0c20fc89b Binary files /dev/null and b/priv/static/static/js/17.c98118b6bb84ee3b5b08.js.map differ diff --git a/priv/static/static/js/18.89c20aa67a4dd067ea37.js b/priv/static/static/js/18.89c20aa67a4dd067ea37.js new file mode 100644 index 000000000..db1c78a49 Binary files /dev/null and b/priv/static/static/js/18.89c20aa67a4dd067ea37.js differ diff --git a/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map b/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map new file mode 100644 index 000000000..72cdf0e0e Binary files /dev/null and b/priv/static/static/js/18.89c20aa67a4dd067ea37.js.map differ diff --git a/priv/static/static/js/19.6e13bad8131c4501c1c5.js b/priv/static/static/js/19.6e13bad8131c4501c1c5.js new file mode 100644 index 000000000..8b32827cc Binary files /dev/null and b/priv/static/static/js/19.6e13bad8131c4501c1c5.js differ diff --git a/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map b/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map new file mode 100644 index 000000000..762d85e27 Binary files /dev/null and b/priv/static/static/js/19.6e13bad8131c4501c1c5.js.map differ diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js b/priv/static/static/js/2.18e4adec273c4ce867a8.js deleted file mode 100644 index d191aa852..000000000 Binary files a/priv/static/static/js/2.18e4adec273c4ce867a8.js and /dev/null differ diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map b/priv/static/static/js/2.18e4adec273c4ce867a8.js.map deleted file mode 100644 index a7f98bfef..000000000 Binary files a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map and /dev/null differ diff --git a/priv/static/static/js/2.78a48aa26599b00c3b8d.js b/priv/static/static/js/2.78a48aa26599b00c3b8d.js new file mode 100644 index 000000000..ecb27aa9c Binary files /dev/null and b/priv/static/static/js/2.78a48aa26599b00c3b8d.js differ diff --git a/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map b/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map new file mode 100644 index 000000000..167cfa1c6 Binary files /dev/null and b/priv/static/static/js/2.78a48aa26599b00c3b8d.js.map differ diff --git a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js b/priv/static/static/js/20.3615c3cea2e1c2707a4f.js new file mode 100644 index 000000000..74f89016c Binary files /dev/null and b/priv/static/static/js/20.3615c3cea2e1c2707a4f.js differ diff --git a/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map b/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map new file mode 100644 index 000000000..acddecea7 Binary files /dev/null and b/priv/static/static/js/20.3615c3cea2e1c2707a4f.js.map differ diff --git a/priv/static/static/js/21.64dedfc646e13e6f7915.js b/priv/static/static/js/21.64dedfc646e13e6f7915.js new file mode 100644 index 000000000..407e6665e Binary files /dev/null and b/priv/static/static/js/21.64dedfc646e13e6f7915.js differ diff --git a/priv/static/static/js/21.64dedfc646e13e6f7915.js.map b/priv/static/static/js/21.64dedfc646e13e6f7915.js.map new file mode 100644 index 000000000..8e3432668 Binary files /dev/null and b/priv/static/static/js/21.64dedfc646e13e6f7915.js.map differ diff --git a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js b/priv/static/static/js/22.6fa63bc6a054b7638e9e.js new file mode 100644 index 000000000..4a8740c99 Binary files /dev/null and b/priv/static/static/js/22.6fa63bc6a054b7638e9e.js differ diff --git a/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map b/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map new file mode 100644 index 000000000..1c556f040 Binary files /dev/null and b/priv/static/static/js/22.6fa63bc6a054b7638e9e.js.map differ diff --git a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js b/priv/static/static/js/23.e0ddea2b6e049d221ee7.js new file mode 100644 index 000000000..51fe36368 Binary files /dev/null and b/priv/static/static/js/23.e0ddea2b6e049d221ee7.js differ diff --git a/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map b/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map new file mode 100644 index 000000000..36bae2bf4 Binary files /dev/null and b/priv/static/static/js/23.e0ddea2b6e049d221ee7.js.map differ diff --git a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js b/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js new file mode 100644 index 000000000..e5abf0af6 Binary files /dev/null and b/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js differ diff --git a/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map b/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map new file mode 100644 index 000000000..09f3c19d0 Binary files /dev/null and b/priv/static/static/js/24.38e3b9d44e9ee703ebf6.js.map differ diff --git a/priv/static/static/js/25.696b41c0a8660e1f85af.js b/priv/static/static/js/25.696b41c0a8660e1f85af.js new file mode 100644 index 000000000..b114890fc Binary files /dev/null and b/priv/static/static/js/25.696b41c0a8660e1f85af.js differ diff --git a/priv/static/static/js/25.696b41c0a8660e1f85af.js.map b/priv/static/static/js/25.696b41c0a8660e1f85af.js.map new file mode 100644 index 000000000..f6d208812 Binary files /dev/null and b/priv/static/static/js/25.696b41c0a8660e1f85af.js.map differ diff --git a/priv/static/static/js/26.1168f22384be75dc5492.js b/priv/static/static/js/26.1168f22384be75dc5492.js new file mode 100644 index 000000000..b77a4d30f Binary files /dev/null and b/priv/static/static/js/26.1168f22384be75dc5492.js differ diff --git a/priv/static/static/js/26.1168f22384be75dc5492.js.map b/priv/static/static/js/26.1168f22384be75dc5492.js.map new file mode 100644 index 000000000..c9b0d8495 Binary files /dev/null and b/priv/static/static/js/26.1168f22384be75dc5492.js.map differ diff --git a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js b/priv/static/static/js/27.3c0cfbb2a898b35486dd.js new file mode 100644 index 000000000..a0765356f Binary files /dev/null and b/priv/static/static/js/27.3c0cfbb2a898b35486dd.js differ diff --git a/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map b/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map new file mode 100644 index 000000000..0cc5f46b2 Binary files /dev/null and b/priv/static/static/js/27.3c0cfbb2a898b35486dd.js.map differ diff --git a/priv/static/static/js/28.926c71d6f1813e177271.js b/priv/static/static/js/28.926c71d6f1813e177271.js new file mode 100644 index 000000000..55cf840f2 Binary files /dev/null and b/priv/static/static/js/28.926c71d6f1813e177271.js differ diff --git a/priv/static/static/js/28.926c71d6f1813e177271.js.map b/priv/static/static/js/28.926c71d6f1813e177271.js.map new file mode 100644 index 000000000..1ae8f08cb Binary files /dev/null and b/priv/static/static/js/28.926c71d6f1813e177271.js.map differ diff --git a/priv/static/static/js/29.187064ebed099ae45749.js b/priv/static/static/js/29.187064ebed099ae45749.js new file mode 100644 index 000000000..6eaae0226 Binary files /dev/null and b/priv/static/static/js/29.187064ebed099ae45749.js differ diff --git a/priv/static/static/js/29.187064ebed099ae45749.js.map b/priv/static/static/js/29.187064ebed099ae45749.js.map new file mode 100644 index 000000000..b5dd63f96 Binary files /dev/null and b/priv/static/static/js/29.187064ebed099ae45749.js.map differ diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js new file mode 100644 index 000000000..6b20ecb04 Binary files /dev/null and b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js differ diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map new file mode 100644 index 000000000..594d9047b Binary files /dev/null and b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map differ diff --git a/priv/static/static/js/30.d78855ca19bf749be905.js b/priv/static/static/js/30.d78855ca19bf749be905.js new file mode 100644 index 000000000..9202d19d3 Binary files /dev/null and b/priv/static/static/js/30.d78855ca19bf749be905.js differ diff --git a/priv/static/static/js/30.d78855ca19bf749be905.js.map b/priv/static/static/js/30.d78855ca19bf749be905.js.map new file mode 100644 index 000000000..b9f39664d Binary files /dev/null and b/priv/static/static/js/30.d78855ca19bf749be905.js.map differ diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js b/priv/static/static/js/4.2d3bef896b463484e6eb.js new file mode 100644 index 000000000..4a611feb4 Binary files /dev/null and b/priv/static/static/js/4.2d3bef896b463484e6eb.js differ diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js.map b/priv/static/static/js/4.2d3bef896b463484e6eb.js.map new file mode 100644 index 000000000..ebcc883e5 Binary files /dev/null and b/priv/static/static/js/4.2d3bef896b463484e6eb.js.map differ diff --git a/priv/static/static/js/5.84f3dce298bc720719c7.js b/priv/static/static/js/5.84f3dce298bc720719c7.js new file mode 100644 index 000000000..242b2a525 Binary files /dev/null and b/priv/static/static/js/5.84f3dce298bc720719c7.js differ diff --git a/priv/static/static/js/5.84f3dce298bc720719c7.js.map b/priv/static/static/js/5.84f3dce298bc720719c7.js.map new file mode 100644 index 000000000..4fcc32982 Binary files /dev/null and b/priv/static/static/js/5.84f3dce298bc720719c7.js.map differ diff --git a/priv/static/static/js/6.b9497e1d403b901a664e.js b/priv/static/static/js/6.b9497e1d403b901a664e.js new file mode 100644 index 000000000..8c66e9062 Binary files /dev/null and b/priv/static/static/js/6.b9497e1d403b901a664e.js differ diff --git a/priv/static/static/js/6.b9497e1d403b901a664e.js.map b/priv/static/static/js/6.b9497e1d403b901a664e.js.map new file mode 100644 index 000000000..5af0576c7 Binary files /dev/null and b/priv/static/static/js/6.b9497e1d403b901a664e.js.map differ diff --git a/priv/static/static/js/7.455b574116ce3f004ffb.js b/priv/static/static/js/7.455b574116ce3f004ffb.js new file mode 100644 index 000000000..0e35f6904 Binary files /dev/null and b/priv/static/static/js/7.455b574116ce3f004ffb.js differ diff --git a/priv/static/static/js/7.455b574116ce3f004ffb.js.map b/priv/static/static/js/7.455b574116ce3f004ffb.js.map new file mode 100644 index 000000000..971b570e1 Binary files /dev/null and b/priv/static/static/js/7.455b574116ce3f004ffb.js.map differ diff --git a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js b/priv/static/static/js/8.8db9f2dcc5ed429777f7.js new file mode 100644 index 000000000..79082fc7c Binary files /dev/null and b/priv/static/static/js/8.8db9f2dcc5ed429777f7.js differ diff --git a/priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map b/priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map new file mode 100644 index 000000000..e193375de Binary files /dev/null and b/priv/static/static/js/8.8db9f2dcc5ed429777f7.js.map differ diff --git a/priv/static/static/js/9.da3973d058660aa9612f.js b/priv/static/static/js/9.da3973d058660aa9612f.js new file mode 100644 index 000000000..50d977f67 Binary files /dev/null and b/priv/static/static/js/9.da3973d058660aa9612f.js differ diff --git a/priv/static/static/js/9.da3973d058660aa9612f.js.map b/priv/static/static/js/9.da3973d058660aa9612f.js.map new file mode 100644 index 000000000..4c8f70599 Binary files /dev/null and b/priv/static/static/js/9.da3973d058660aa9612f.js.map differ diff --git a/priv/static/static/js/app.31bba9f1e242ff273dcb.js b/priv/static/static/js/app.31bba9f1e242ff273dcb.js new file mode 100644 index 000000000..22413689c Binary files /dev/null and b/priv/static/static/js/app.31bba9f1e242ff273dcb.js differ diff --git a/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map b/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map new file mode 100644 index 000000000..8ff7a00c6 Binary files /dev/null and b/priv/static/static/js/app.31bba9f1e242ff273dcb.js.map differ diff --git a/priv/static/static/js/app.838ffa9aecf210c7d744.js b/priv/static/static/js/app.838ffa9aecf210c7d744.js deleted file mode 100644 index 7e224748e..000000000 Binary files a/priv/static/static/js/app.838ffa9aecf210c7d744.js and /dev/null differ diff --git a/priv/static/static/js/app.838ffa9aecf210c7d744.js.map b/priv/static/static/js/app.838ffa9aecf210c7d744.js.map deleted file mode 100644 index 4c2835cb4..000000000 Binary files a/priv/static/static/js/app.838ffa9aecf210c7d744.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js b/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js deleted file mode 100644 index d1f1a1830..000000000 Binary files a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js and /dev/null differ diff --git a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map b/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map deleted file mode 100644 index 0d4a859ea..000000000 Binary files a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map and /dev/null differ diff --git a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js b/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js new file mode 100644 index 000000000..76c8a8dc1 Binary files /dev/null and b/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js differ diff --git a/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map b/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map new file mode 100644 index 000000000..f3c067c15 Binary files /dev/null and b/priv/static/static/js/vendors~app.9e24ed238da5a8538f50.js.map differ diff --git a/priv/static/static/terms-of-service.html b/priv/static/static/terms-of-service.html index a6da539e4..3b6bbb36b 100644 --- a/priv/static/static/terms-of-service.html +++ b/priv/static/static/terms-of-service.html @@ -1,4 +1,9 @@

Terms of Service

-

This is a placeholder ToS. Edit "/static/terms-of-service.html" to make it fit the needs of your instance.

+

This is the default placeholder ToS. You should copy it over to your static folder and edit it to fit the needs of your instance.

+ +

To do so, place a file at "/instance/static/static/terms-of-service.html" in your + Pleroma install containing the real ToS for your instance.

+

See the Pleroma documentation for more information.

+
diff --git a/priv/static/static/themes/redmond-xx-se.json b/priv/static/static/themes/redmond-xx-se.json index 7a4a29da3..24480d2c7 100644 --- a/priv/static/static/themes/redmond-xx-se.json +++ b/priv/static/static/themes/redmond-xx-se.json @@ -286,7 +286,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/priv/static/static/themes/redmond-xx.json b/priv/static/static/themes/redmond-xx.json index ff95b1e06..cf9010fe2 100644 --- a/priv/static/static/themes/redmond-xx.json +++ b/priv/static/static/themes/redmond-xx.json @@ -277,7 +277,9 @@ "cGreen": "#008000", "cOrange": "#808000", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/priv/static/static/themes/redmond-xxi.json b/priv/static/static/themes/redmond-xxi.json index f788bdb83..7fdc4a6d6 100644 --- a/priv/static/static/themes/redmond-xxi.json +++ b/priv/static/static/themes/redmond-xxi.json @@ -259,7 +259,9 @@ "cGreen": "#669966", "cOrange": "#cc6633", "highlight": "--accent", - "selectedPost": "--bg,-10" + "selectedPost": "--bg,-10", + "selectedMenu": "--accent", + "selectedMenuPopover": "--accent" }, "radii": { "btn": "0", diff --git a/priv/static/sw-pleroma-workbox.js b/priv/static/sw-pleroma-workbox.js new file mode 100644 index 000000000..0b39d0963 Binary files /dev/null and b/priv/static/sw-pleroma-workbox.js differ diff --git a/priv/static/sw-pleroma-workbox.js.map b/priv/static/sw-pleroma-workbox.js.map new file mode 100644 index 000000000..e35c07e72 Binary files /dev/null and b/priv/static/sw-pleroma-workbox.js.map differ diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index 4d73c414e..9b7d127fd 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map index c704cb951..5749809d5 100644 Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ diff --git a/priv/static/sw.js b/priv/static/sw.js index 0fde0f440..5605bb05e 100644 Binary files a/priv/static/sw.js and b/priv/static/sw.js differ diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs index e899d4509..d75c06cc7 100644 --- a/test/activity_expiration_test.exs +++ b/test/activity_expiration_test.exs @@ -44,7 +44,7 @@ test "deletes an expiration activity" do %{activity_id: activity.id, scheduled_at: naive_datetime} ) - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid) + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) refute Pleroma.Repo.get(Pleroma.Activity, activity.id) refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) diff --git a/test/config/deprecation_warnings_test.exs b/test/config/deprecation_warnings_test.exs index 548ee87b0..555661a71 100644 --- a/test/config/deprecation_warnings_test.exs +++ b/test/config/deprecation_warnings_test.exs @@ -54,4 +54,12 @@ test "move_namespace_and_warn/2" do assert Pleroma.Config.get(new_group2) == 2 assert Pleroma.Config.get(new_group3) == 3 end + + test "check_media_proxy_whitelist_config/0" do + clear_config([:media_proxy, :whitelist], ["https://example.com", "example2.com"]) + + assert capture_log(fn -> + Pleroma.Config.DeprecationWarnings.check_media_proxy_whitelist_config() + end) =~ "Your config is using old format (only domain) for MediaProxy whitelist option" + end end diff --git a/test/config/holder_test.exs b/test/config/holder_test.exs index 15d48b5c7..abcaa27dd 100644 --- a/test/config/holder_test.exs +++ b/test/config/holder_test.exs @@ -10,7 +10,6 @@ defmodule Pleroma.Config.HolderTest do test "default_config/0" do config = Holder.default_config() assert config[:pleroma][Pleroma.Uploaders.Local][:uploads] == "test/uploads" - assert config[:tesla][:adapter] == Tesla.Mock refute config[:pleroma][Pleroma.Repo] refute config[:pleroma][Pleroma.Web.Endpoint] @@ -18,17 +17,15 @@ test "default_config/0" do refute config[:pleroma][:configurable_from_database] refute config[:pleroma][:database] refute config[:phoenix][:serve_endpoints] + refute config[:tesla][:adapter] end test "default_config/1" do pleroma_config = Holder.default_config(:pleroma) assert pleroma_config[Pleroma.Uploaders.Local][:uploads] == "test/uploads" - tesla_config = Holder.default_config(:tesla) - assert tesla_config[:adapter] == Tesla.Mock end test "default_config/2" do assert Holder.default_config(:pleroma, Pleroma.Uploaders.Local) == [uploads: "test/uploads"] - assert Holder.default_config(:tesla, :adapter) == Tesla.Mock end end diff --git a/test/docs/generator_test.exs b/test/docs/generator_test.exs index 9c9f4357b..b32918a69 100644 --- a/test/docs/generator_test.exs +++ b/test/docs/generator_test.exs @@ -13,21 +13,13 @@ defmodule Pleroma.Docs.GeneratorTest do key: :uploader, type: :module, description: "", - suggestions: - Generator.list_modules_in_dir( - "lib/pleroma/upload/filter", - "Elixir.Pleroma.Upload.Filter." - ) + suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter} }, %{ key: :filters, type: {:list, :module}, description: "", - suggestions: - Generator.list_modules_in_dir( - "lib/pleroma/web/activity_pub/mrf", - "Elixir.Pleroma.Web.ActivityPub.MRF." - ) + suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF} }, %{ key: Pleroma.Upload, diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs index bc871a0a9..9082ae5a7 100644 --- a/test/emails/admin_email_test.exs +++ b/test/emails/admin_email_test.exs @@ -31,7 +31,7 @@ test "build report email" do account_url }\">#{account.nickname}

\n

Comment: Test comment\n

Statuses:\n

\n

\n\n" + }\">#{status_url}\n \n

\n\n

\nView Reports in AdminFE\n" end test "it works when the reporter is a remote user without email" do diff --git a/test/filter_test.exs b/test/filter_test.exs index 63a30c736..0a5c4426a 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -3,37 +3,39 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.FilterTest do - alias Pleroma.Repo use Pleroma.DataCase import Pleroma.Factory + alias Pleroma.Filter + alias Pleroma.Repo + describe "creating filters" do test "creating one filter" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, filter_id: 42, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) - result = Pleroma.Filter.get(filter.filter_id, user) + {:ok, %Filter{} = filter} = Filter.create(query) + result = Filter.get(filter.filter_id, user) assert query.phrase == result.phrase end test "creating one filter without a pre-defined filter_id" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) + {:ok, %Filter{} = filter} = Filter.create(query) # Should start at 1 assert filter.filter_id == 1 end @@ -41,23 +43,23 @@ test "creating one filter without a pre-defined filter_id" do test "creating additional filters uses previous highest filter_id + 1" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 42, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + {:ok, %Filter{} = filter_one} = Filter.create(query_one) - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user.id, # No filter_id phrase: "who", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + {:ok, %Filter{} = filter_two} = Filter.create(query_two) assert filter_two.filter_id == filter_one.filter_id + 1 end @@ -65,29 +67,29 @@ test "filter_id is unique per user" do user_one = insert(:user) user_two = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user_one.id, phrase: "knights", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + {:ok, %Filter{} = filter_one} = Filter.create(query_one) - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user_two.id, phrase: "who", context: ["home"] } - {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + {:ok, %Filter{} = filter_two} = Filter.create(query_two) assert filter_one.filter_id == 1 assert filter_two.filter_id == 1 - result_one = Pleroma.Filter.get(filter_one.filter_id, user_one) + result_one = Filter.get(filter_one.filter_id, user_one) assert result_one.phrase == filter_one.phrase - result_two = Pleroma.Filter.get(filter_two.filter_id, user_two) + result_two = Filter.get(filter_two.filter_id, user_two) assert result_two.phrase == filter_two.phrase end end @@ -95,38 +97,38 @@ test "filter_id is unique per user" do test "deleting a filter" do user = insert(:user) - query = %Pleroma.Filter{ + query = %Filter{ user_id: user.id, filter_id: 0, phrase: "knights", context: ["home"] } - {:ok, _filter} = Pleroma.Filter.create(query) - {:ok, filter} = Pleroma.Filter.delete(query) - assert is_nil(Repo.get(Pleroma.Filter, filter.filter_id)) + {:ok, _filter} = Filter.create(query) + {:ok, filter} = Filter.delete(query) + assert is_nil(Repo.get(Filter, filter.filter_id)) end test "getting all filters by an user" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 1, phrase: "knights", context: ["home"] } - query_two = %Pleroma.Filter{ + query_two = %Filter{ user_id: user.id, filter_id: 2, phrase: "who", context: ["home"] } - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.create(query_two) - filters = Pleroma.Filter.get_filters(user) + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.create(query_two) + filters = Filter.get_filters(user) assert filter_one in filters assert filter_two in filters end @@ -134,7 +136,7 @@ test "getting all filters by an user" do test "updating a filter" do user = insert(:user) - query_one = %Pleroma.Filter{ + query_one = %Filter{ user_id: user.id, filter_id: 1, phrase: "knights", @@ -146,8 +148,9 @@ test "updating a filter" do context: ["home", "timeline"] } - {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes) + {:ok, filter_one} = Filter.create(query_one) + {:ok, filter_two} = Filter.update(filter_one, changes) + assert filter_one != filter_two assert filter_two.phrase == changes.phrase assert filter_two.context == changes.context diff --git a/test/fixtures/DSCN0010.jpg b/test/fixtures/DSCN0010.jpg new file mode 100644 index 000000000..4a2c1552b Binary files /dev/null and b/test/fixtures/DSCN0010.jpg differ diff --git a/test/fixtures/fetch_mocks/104410921027210069.json b/test/fixtures/fetch_mocks/104410921027210069.json new file mode 100644 index 000000000..583f7a4dc --- /dev/null +++ b/test/fixtures/fetch_mocks/104410921027210069.json @@ -0,0 +1,72 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + { + "atomUri" : "ostatus:atomUri", + "conversation" : "ostatus:conversation", + "inReplyToAtomUri" : "ostatus:inReplyToAtomUri", + "ostatus" : "http://ostatus.org#", + "sensitive" : "as:sensitive", + "toot" : "http://joinmastodon.org/ns#", + "votersCount" : "toot:votersCount" + } + ], + "atomUri" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069", + "attachment" : [], + "attributedTo" : "https://busshi.moe/users/tuxcrafting", + "cc" : [ + "https://busshi.moe/users/tuxcrafting/followers", + "https://stereophonic.space/users/fixpoint", + "https://blob.cat/users/blobyoumu", + "https://cawfee.club/users/grips", + "https://jaeger.website/users/igel" + ], + "content" : "

@fixpoint @blobyoumu @grips @igel there's a difference between not liking nukes and not liking nuclear power
nukes are pretty bad as are all WMDs in general but disliking nuclear power just indicates you are unable of thought

", + "contentMap" : { + "en" : "

@fixpoint @blobyoumu @grips @igel there's a difference between not liking nukes and not liking nuclear power
nukes are pretty bad as are all WMDs in general but disliking nuclear power just indicates you are unable of thought

" + }, + "conversation" : "https://cawfee.club/contexts/ad6c73d8-efc2-4e74-84ea-2dacf1a27a5e", + "id" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069", + "inReplyTo" : "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17", + "inReplyToAtomUri" : "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17", + "published" : "2020-06-26T15:10:19Z", + "replies" : { + "first" : { + "items" : [], + "next" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069/replies?only_other_accounts=true&page=true", + "partOf" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069/replies", + "type" : "CollectionPage" + }, + "id" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069/replies", + "type" : "Collection" + }, + "sensitive" : false, + "summary" : null, + "tag" : [ + { + "href" : "https://stereophonic.space/users/fixpoint", + "name" : "@fixpoint@stereophonic.space", + "type" : "Mention" + }, + { + "href" : "https://blob.cat/users/blobyoumu", + "name" : "@blobyoumu@blob.cat", + "type" : "Mention" + }, + { + "href" : "https://cawfee.club/users/grips", + "name" : "@grips@cawfee.club", + "type" : "Mention" + }, + { + "href" : "https://jaeger.website/users/igel", + "name" : "@igel@jaeger.website", + "type" : "Mention" + } + ], + "to" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" : "Note", + "url" : "https://busshi.moe/@tuxcrafting/104410921027210069" +} diff --git a/test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json b/test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json new file mode 100644 index 000000000..0226b058a --- /dev/null +++ b/test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json @@ -0,0 +1,59 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://social.sakamoto.gq/schemas/litepub-0.1.jsonld", + { + "@language" : "und" + } + ], + "actor" : "https://social.sakamoto.gq/users/eal", + "attachment" : [], + "attributedTo" : "https://social.sakamoto.gq/users/eal", + "cc" : [ + "https://social.sakamoto.gq/users/eal/followers" + ], + "content" : "@tuxcrafting @fixpoint @blobyoumu @grips @igel What's bad about nukes?", + "context" : "https://cawfee.club/contexts/ad6c73d8-efc2-4e74-84ea-2dacf1a27a5e", + "conversation" : "https://cawfee.club/contexts/ad6c73d8-efc2-4e74-84ea-2dacf1a27a5e", + "id" : "https://social.sakamoto.gq/objects/f20f2497-66d9-4a52-a2e1-1be2a39c32c1", + "inReplyTo" : "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069", + "published" : "2020-06-26T15:20:15.975737Z", + "sensitive" : false, + "summary" : "", + "tag" : [ + { + "href" : "https://blob.cat/users/blobyoumu", + "name" : "@blobyoumu@blob.cat", + "type" : "Mention" + }, + { + "href" : "https://busshi.moe/users/tuxcrafting", + "name" : "@tuxcrafting@busshi.moe", + "type" : "Mention" + }, + { + "href" : "https://cawfee.club/users/grips", + "name" : "@grips@cawfee.club", + "type" : "Mention" + }, + { + "href" : "https://jaeger.website/users/igel", + "name" : "@igel@jaeger.website", + "type" : "Mention" + }, + { + "href" : "https://stereophonic.space/users/fixpoint", + "name" : "@fixpoint@stereophonic.space", + "type" : "Mention" + } + ], + "to" : [ + "https://busshi.moe/users/tuxcrafting", + "https://www.w3.org/ns/activitystreams#Public", + "https://blob.cat/users/blobyoumu", + "https://stereophonic.space/users/fixpoint", + "https://cawfee.club/users/grips", + "https://jaeger.website/users/igel" + ], + "type" : "Note" +} diff --git a/test/fixtures/fetch_mocks/eal.json b/test/fixtures/fetch_mocks/eal.json new file mode 100644 index 000000000..a605476e6 --- /dev/null +++ b/test/fixtures/fetch_mocks/eal.json @@ -0,0 +1,43 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://social.sakamoto.gq/schemas/litepub-0.1.jsonld", + { + "@language" : "und" + } + ], + "attachment" : [], + "discoverable" : true, + "endpoints" : { + "oauthAuthorizationEndpoint" : "https://social.sakamoto.gq/oauth/authorize", + "oauthRegistrationEndpoint" : "https://social.sakamoto.gq/api/v1/apps", + "oauthTokenEndpoint" : "https://social.sakamoto.gq/oauth/token", + "sharedInbox" : "https://social.sakamoto.gq/inbox", + "uploadMedia" : "https://social.sakamoto.gq/api/ap/upload_media" + }, + "followers" : "https://social.sakamoto.gq/users/eal/followers", + "following" : "https://social.sakamoto.gq/users/eal/following", + "icon" : { + "type" : "Image", + "url" : "https://social.sakamoto.gq/media/f1cb6f79bf6839f3223ca240441f766056b74ddd23c69bcaf8bb1ba1ecff6eec.jpg" + }, + "id" : "https://social.sakamoto.gq/users/eal", + "image" : { + "type" : "Image", + "url" : "https://social.sakamoto.gq/media/e5cccf26421e8366f4e34be3c9d5042b8bc8dcceccc7c8e89785fa312dd9632c.jpg" + }, + "inbox" : "https://social.sakamoto.gq/users/eal/inbox", + "manuallyApprovesFollowers" : false, + "name" : "에알", + "outbox" : "https://social.sakamoto.gq/users/eal/outbox", + "preferredUsername" : "eal", + "publicKey" : { + "id" : "https://social.sakamoto.gq/users/eal#main-key", + "owner" : "https://social.sakamoto.gq/users/eal", + "publicKeyPem" : "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3pF85YOhhv2Zaxv9YQ7\nrCe1aEhetCMVHtrK63tUVGoGdsblyKnVeJNbFcr6k3y35OpHS3HXIi6GzgihYcTu\nONLP4eQMHTnLUNAQZi03mjJA4iIq8v/tm8ZkL2mXsQSAbWj6Iq518mHNN7OvCoNt\n3Xjepl/0kgkc2gsund7m8r+Wu0Fusx6UlUyyAk3PexdDRdSSlVLeskqtP8jtdQDo\nL70pMyL+VD+Qb9RKFdtgJ+M4OqYP+7FVzCqXN0QIPhFf/kvHSLr+c4Y3Wm0nAKHU\n9CwXWXz5Xqscpv41KlgnUCOkTXb5eBSt23lNulae5srVzWBiFb6guiCpNzBGa+Sq\nrwIDAQAB\n-----END PUBLIC KEY-----\n\n" + }, + "summary" : "Pizza napoletana supremacist.

Any artworks posted here that are good are not mine.", + "tag" : [], + "type" : "Person", + "url" : "https://social.sakamoto.gq/users/eal" +} diff --git a/test/fixtures/fetch_mocks/tuxcrafting.json b/test/fixtures/fetch_mocks/tuxcrafting.json new file mode 100644 index 000000000..5dce2a16d --- /dev/null +++ b/test/fixtures/fetch_mocks/tuxcrafting.json @@ -0,0 +1,59 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "IdentityProof" : "toot:IdentityProof", + "PropertyValue" : "schema:PropertyValue", + "alsoKnownAs" : { + "@id" : "as:alsoKnownAs", + "@type" : "@id" + }, + "discoverable" : "toot:discoverable", + "featured" : { + "@id" : "toot:featured", + "@type" : "@id" + }, + "focalPoint" : { + "@container" : "@list", + "@id" : "toot:focalPoint" + }, + "manuallyApprovesFollowers" : "as:manuallyApprovesFollowers", + "movedTo" : { + "@id" : "as:movedTo", + "@type" : "@id" + }, + "schema" : "http://schema.org#", + "toot" : "http://joinmastodon.org/ns#", + "value" : "schema:value" + } + ], + "attachment" : [], + "discoverable" : true, + "endpoints" : { + "sharedInbox" : "https://busshi.moe/inbox" + }, + "featured" : "https://busshi.moe/users/tuxcrafting/collections/featured", + "followers" : "https://busshi.moe/users/tuxcrafting/followers", + "following" : "https://busshi.moe/users/tuxcrafting/following", + "icon" : { + "mediaType" : "image/jpeg", + "type" : "Image", + "url" : "https://blobcdn.busshi.moe/busshifiles/accounts/avatars/000/046/872/original/054f0806ccb303d0.jpg" + }, + "id" : "https://busshi.moe/users/tuxcrafting", + "inbox" : "https://busshi.moe/users/tuxcrafting/inbox", + "manuallyApprovesFollowers" : true, + "name" : "@tuxcrafting@localhost:8080", + "outbox" : "https://busshi.moe/users/tuxcrafting/outbox", + "preferredUsername" : "tuxcrafting", + "publicKey" : { + "id" : "https://busshi.moe/users/tuxcrafting#main-key", + "owner" : "https://busshi.moe/users/tuxcrafting", + "publicKeyPem" : "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwqWWTBf9OizsBiBhGS/M\nQTT6fB1VvQP6vvxouGZ5cGg1a97V67ouhjJ+nGMuWr++DNYjJYkk2TOynfykk0H/\n8rRSujSe3BNRKYGNzdnRJu/4XxgIE847Fqx5SijSP23JGYcn8TjeSUsN2u2YYVXK\n+Eb3Bu7DjGiqwNon6YB0h5qkGjkMSMVIFn0hZx6Z21bkfYWgra96Ok5OWf7Ck3je\nCuErlCMZcbQcHtFpBueJAxYchjNvm6fqwZxLX/NtaHdr7Fm2kin89mqzliapBlFH\nCXk7Jln6xV5I6ryggPAMzm3fuHzeo0RWlu8lrxLfARBVwaQQZS99bwqp6N9O2aUp\nYwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "summary" : "

expert procrastinator

trans(humanist|gender|istorized)

web: https://tuxcrafting.port0.org
pronouns: she/they
languages: french (native)/english (fluent)/hebrew (ok-ish)/esperanto (barely)

", + "tag" : [], + "type" : "Person", + "url" : "https://busshi.moe/@tuxcrafting" +} diff --git a/test/fixtures/preload_static/instance/panel.html b/test/fixtures/preload_static/instance/panel.html new file mode 100644 index 000000000..fc58e4e93 --- /dev/null +++ b/test/fixtures/preload_static/instance/panel.html @@ -0,0 +1 @@ +HEY! diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index 9fdd6557c..a911b979a 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -26,6 +26,9 @@ "summary": "\u003cp\u003e\u003c/p\u003e", "url": "http://mastodon.example.org/@admin", "manuallyApprovesFollowers": false, + "capabilities": { + "acceptsChatMessages": true + }, "publicKey": { "id": "http://mastodon.example.org/users/admin#main-key", "owner": "http://mastodon.example.org/users/admin", diff --git a/test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json b/test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json index 3f3f0f4fb..b76ba96a5 100644 --- a/test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json +++ b/test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json @@ -1 +1,227 @@ -{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Emoji":"toot:Emoji","Hashtag":"as:Hashtag","atomUri":"ostatus:atomUri","conversation":"ostatus:conversation","featured":"toot:featured","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"inReplyToAtomUri":"ostatus:inReplyToAtomUri","manuallyApprovesFollowers":"as:manuallyApprovesFollowers","movedTo":"as:movedTo","ostatus":"http://ostatus.org#","sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#"}],"attributedTo":["https://baptiste.gelez.xyz/@/BaptisteGelez"],"cc":[],"content":"

It has been one month since the last \"This Month in Plume\" article, so it is time for another edition of our monthly changelog!

\n

Bug Fixes and Security

\n

Let's start with the hidden, but still (very) important changes: bug fixes and security patches.

\n

First of all, @Trinity protected us against two major security flaws, called XSS and CSRF. The first one allows the attacker to run malicious code if you visit a Plume page where some of their personal data is present. The second one lets them post data with your Plume account by visiting one of their own website. It is two very common attack, and it is great we are now protected against them!

\n

The other big change in this area, is that we are now validating the data you are sending before doing anything with it. It means that, for instance, you will no longer be able to register with an empty username and to break everything.

\n

On the federation side, many issues were reported by @kaniini and redmatrix (respectively contributing to Pleroma and Hubzilla). By fixing some of them, we made it possible to federate Plume articles to Pleroma!

\n

@Trinity hopefully noticed that there was a bug in our password check code: we were not checking that your password was correct, but only that the verification process went without errors. Concretely, it means that you could login to any account with any password. I wrote this part of the code when I was still the only contributor to the project, so nobody could review my work. We will now be trying to check every change, especially when it deals with critical parts of Plume, to avoid similar issues in the future, and we I'm really sorry this happened (even if I think nobody exploited it).

\n

Zanfib and stephenburgess8 also commited some small bugfixes, improving the general experience.

\n

New Features

\n

Let's now talk about the features that we introduced during this month.

\n

One of the most easy to spot is the redesign of Plume, made by @Madeorsk. I personaly love what he did, it really improved the readability and gave Plume a bit more of identity than the previous design. And he is still improving it.

\n

We also enabled Mardown in comment, to let you write more structured and nicely formatted responses.

\n

As you may have noticed, I have used mentions in this post. Indeed, it is now possible to mention someone in your articles or in comments. It works exactly the same way as in other apps, and you should receive a notification if someone mentionned you.

\n

A dashboard to manage your blogs has also been introduced. In the future it may be used to manage your drafts, and eventually to show some statistics. The goal is to have a more specific homepage for authors.

\n

The federation with other ActivityPub softwares, like Mastodon or Pleroma is starting to work quite well, but the federation between Plume instances is far from being complete. However, we started to work on it, and it is now possible to view a distant user profile or blog from your instance, even if only basic informations are fetched yet (the articles are not loaded for instance).

\n

Another new feature that may not be visible for everyone, is the new NodeInfo endpoint. NodeInfo is a protocol allowing to get informations about a specific federated instance (whatever software it runs). It means that Plume instances can now be listed on sites like fediverse.network.

\n

Maybe you wanted to host a Plume instance, but you don't like long install process during which you are just copy/pasting commands that you don't really understand from the documentation. That's why we introduced a setup script: the first you'll launch Plume, it will ask you a few questions and automatically setup your instance in a few minutes. We hope that this feature will help to host small instances, run by non-professional adminsys. You can see a demo of this tool on asciinema.

\n

Last but not least, Plume is now translatable! It is already available in English, French, Polish (thanks to @m4sk1n)) and German (thanks to bitkeks). If your browser is configured to display pages in these languages, you should normally see the interface in your language. And if your language is not present yet, feel free to add your translation.

\n

Other Changes

\n

We also improved the code a lot. We tried to separate each part as much as possible, making it easier to re-use for other projects. For instance, our database code is now isolated from the rest of the app, which means it will be easier to make import tools from other blogging engines. Some parts of the code are even shared with another project, Aardwolf a federated Facebook alternative. For instance, both of our projects use the same internationalization code, and once Aardwolf will implement federation, this part of the code will probably be shared too. Since the WebFinger module (used to find new users and blogs) and the CSRF protection code (see the \"Bug fixes and Security\" section) have been isolated in their own modules, they may be shared by both projects too.

\n

We also worked a lot on documentation. We now have articles explaining how to setup your Plume instance on various operating systems, but also documenting the translation process. I want to thank BanjoFox (who imported some documentation from their project, Aardwolf, as the setup is quite similar), Kushal and @gled@plume.mastodon.host for working on this.

\n

As you can see, there were many changes this month, but there still a lot to do. Your help will of course be welcome. If you want to contribute to the code, translate Plume in your language, write some documentation, or anything else (or even if you're just curious about the project), feel free to join our Matrix room: #plume:disroot.org. Otherwise, as BanjoFox said on the Aardwolf Team Mastodon account, talking about the project around you is one of the easiest way to help.

\n","id":"https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/","likes":null,"name":"This Month in Plume: June 2018","published":"2018-07-10T20:16:24.087622Z","shares":null,"source":null,"tag":[{"href":"https://baptiste.gelez.xyz/@/Trinity","name":"@Trinity","type":"Mention"},{"href":"https://baptiste.gelez.xyz/@/kaniini/","name":"@kaniini","type":"Mention"},{"href":"https://baptiste.gelez.xyz/@/Trinity","name":"@Trinity","type":"Mention"}],"to":["https://unixcorn.xyz/users/Bat","https://mastodon.host/users/federationbot","https://social.tcit.fr/users/tcit","https://framapiaf.org/users/qwerty","https://mastodon.social/users/lthms","https://eldritch.cafe/users/Nausicaa","https://imaginair.es/users/Elanndelh","https://framapiaf.org/users/Drulac","https://mastodon.partipirate.org/users/NicolasConstant","https://aleph.land/users/Madeorsk","https://maly.io/users/Troll","https://hostux.social/users/superjey","https://mamot.fr/users/Phigger","https://mastodon.social/users/wakest","https://social.coop/users/wakest","https://unixcorn.xyz/users/Ce_lo","https://social.art-software.fr/users/Electron","https://framapiaf.org/users/Quenti","https://toot.plus.yt/users/Djyp","https://mastodon.social/users/brainblasted","https://social.mochi.academy/users/Ambraven","https://social.hacktivis.me/users/lanodan","https://mastodon.eliotberriot.com/users/eliotberriot","https://edolas.world/users/0x1C3B00DA","https://toot.cafe/users/zack","https://manowar.social/users/zatnosk","https://eldritch.cafe/users/fluffy","https://mastodon.social/users/david_ross","https://kosmos.social/users/xiroux","https://mastodon.art/users/EmergencyBattle","https://mastodon.social/users/trwnh","https://octodon.social/users/pybyte","https://anticapitalist.party/users/Trinity","https://mstdn.mx/users/xavavu","https://baptiste.gelez.xyz/@/m4sk1n","https://eldritch.cafe/users/milia","https://mastodon.zaclys.com/users/arx","https://toot.cafe/users/sivy","https://mastodon.social/users/ortegacmanuel","https://mastodon.observer/users/stephen","https://octodon.social/users/chloe","https://unixcorn.xyz/users/AmauryPi","https://cybre.space/users/rick_777","https://mastodon.social/users/wezm","https://baptiste.gelez.xyz/@/idlesong","https://mamot.fr/users/dr4Ke","https://imaginair.es/users/Phigger","https://mamot.fr/users/dlink","https://anticapitalist.party/users/a000d4f7a91939d0e71df1646d7a48","https://framapiaf.org/users/PhieLaidMignon","https://mastodon.social/users/y6nH","https://crazynoisybizarre.town/users/FederationBot","https://social.weho.st/users/dvn","https://mastodon.art/users/Wolthera","https://diaspodon.fr/users/dada","https://pachyder.me/users/Lanza","https://mastodon.xyz/users/ag","https://aleph.land/users/yahananxie","https://mstdn.io/users/chablis_social","https://mastodon.gougere.fr/users/fabien","https://functional.cafe/users/otini","https://social.coop/users/bhaugen","https://octodon.social/users/donblanco","https://chaos.social/users/astro","https://pachyder.me/users/sibear","https://mamot.fr/users/yohann","https://social.wxcafe.net/users/Bat","https://mastodon.social/users/dansup","https://chaos.social/users/juh","https://scifi.fyi/users/paeneultima","https://hostux.social/users/Deuchnord","https://mstdn.fr/users/taziden","https://mamot.fr/users/PifyZ","https://mastodon.social/users/plantabaja","https://mastodon.social/users/gitzgrog","https://mastodon.social/users/Syluban","https://masto.pt/users/eloisa","https://pleroma.soykaf.com/users/notclacke","https://mastodon.social/users/SiegfriedEhret","https://writing.exchange/users/write_as","https://mstdn.io/users/shellkr","https://mastodon.uy/users/jorge","https://mastodon.technology/users/bobstechsite","https://mastodon.social/users/hinterwaeldler","https://mastodon.xyz/users/mgdelacroix","https://mastodon.cloud/users/jjatria","https://baptiste.gelez.xyz/@/Jade/","https://edolas.world/users/pfm","https://mstdn.io/users/jort","https://mastodon.social/users/andreipetcu","https://mastodon.technology/users/0xf00fc7c8","https://mastodon.social/users/khanate","https://mastodon.technology/users/francois","https://mastodon.social/users/glherrmann","https://mastodon.host/users/gled","https://social.holdmybeer.solutions/users/kemonine","https://scholar.social/users/bgcarlisle","https://mastodon.social/users/oldgun","https://baptiste.gelez.xyz/@/snoe/","https://mastodon.at/users/switchingsocial","https://scifi.fyi/users/BrokenBiscuit","https://dev.glitch.social/users/hoodie","https://todon.nl/users/paulfree14","https://mastodon.social/users/aadilayub","https://social.fsck.club/users/anarchosaurus","https://mastodonten.de/users/GiantG","https://mastodon.technology/users/cj","https://cybre.space/users/sam","https://layer8.space/users/silkevicious","https://mastodon.xyz/users/Jimmyrwx","https://fosstodon.org/users/danyspin97","https://mstdn.io/users/cristhyano","https://mastodon.social/users/vanyok","https://hulvr.com/users/rook","https://niu.moe/users/Lucifer","https://mamot.fr/users/Thibaut","https://mastodont.cat/users/bgta","https://mstdn.io/users/hontoni","https://niu.moe/users/lionirdeadman","https://functional.cafe/users/phoe","https://mastodon.social/users/toontoet","https://mastodon.social/users/danipozo","https://scholar.social/users/robertson","https://mastodon.social/users/aldatsa","https://elekk.xyz/users/maloki","https://kitty.town/users/nursemchurt","https://neigh.horse/users/commagray","https://mastodon.social/users/hirojin","https://mastodon.xyz/users/mareklach","https://chaos.social/users/benthor","https://mastodon.social/users/djperreault","https://mastodon.art/users/eylul","https://mastodon.opportunis.me/users/bob","https://tootplanet.space/users/Shutsumon","https://toot.cat/users/woozle","https://mastodon.social/users/StephenLB","https://sleeping.town/users/oct2pus","https://mastodon.indie.host/users/stragu","https://social.coop/users/gilscottfitzgerald","https://icosahedron.website/users/joeld","https://mastodon.social/users/hellion","https://cybre.space/users/cooler_ranch","https://mastodon.social/users/kelsonv","https://mastodon.lat/users/scalpol","https://writing.exchange/users/hnb","https://hex.bz/users/Horst","https://mastodon.social/users/weddle","https://maly.io/users/sonya","https://social.coop/users/medusa","https://mastodon.social/users/DystopianK","https://mstdn.io/users/d_io","https://fosstodon.org/users/brandon","https://fosstodon.org/users/Cando","https://mastodon.host/users/panina","https://floss.social/users/tuxether","https://social.tchncs.de/users/suitbertmonz","https://mastodon.social/users/jrt","https://mastodon.social/users/sirikon","https://mstdn.io/users/yabirgb","https://mastodon.cloud/users/FerdiZ","https://mastodon.social/users/carlchenet","https://social.polonkai.eu/users/calendar_social","https://social.polonkai.eu/users/gergely","https://mastodon.social/users/Jelv","https://mastodon.social/users/srinicame","https://cybre.space/users/mastoabed","https://mastodon.social/users/tagomago","https://lgbt.io/users/bootblackCub","https://niu.moe/users/Nopplyy","https://mastodon.social/users/bpugh","https://www.w3.org/ns/activitystreams#Public"],"type":"Article","uploadMedia":null,"url":"https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"} \ No newline at end of file +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji" : "toot:Emoji", + "Hashtag" : "as:Hashtag", + "atomUri" : "ostatus:atomUri", + "conversation" : "ostatus:conversation", + "featured" : "toot:featured", + "focalPoint" : { + "@container" : "@list", + "@id" : "toot:focalPoint" + }, + "inReplyToAtomUri" : "ostatus:inReplyToAtomUri", + "manuallyApprovesFollowers" : "as:manuallyApprovesFollowers", + "movedTo" : "as:movedTo", + "ostatus" : "http://ostatus.org#", + "sensitive" : "as:sensitive", + "toot" : "http://joinmastodon.org/ns#" + } + ], + "attributedTo" : [ + "https://baptiste.gelez.xyz/@/BaptisteGelez" + ], + "cc" : [], + "content" : "

It has been one month since the last \"This Month in Plume\" article, so it is time for another edition of our monthly changelog!

\n

Bug Fixes and Security

\n

Let's start with the hidden, but still (very) important changes: bug fixes and security patches.

\n

First of all, @Trinity protected us against two major security flaws, called XSS and CSRF. The first one allows the attacker to run malicious code if you visit a Plume page where some of their personal data is present. The second one lets them post data with your Plume account by visiting one of their own website. It is two very common attack, and it is great we are now protected against them!

\n

The other big change in this area, is that we are now validating the data you are sending before doing anything with it. It means that, for instance, you will no longer be able to register with an empty username and to break everything.

\n

On the federation side, many issues were reported by @kaniini and redmatrix (respectively contributing to Pleroma and Hubzilla). By fixing some of them, we made it possible to federate Plume articles to Pleroma!

\n

@Trinity hopefully noticed that there was a bug in our password check code: we were not checking that your password was correct, but only that the verification process went without errors. Concretely, it means that you could login to any account with any password. I wrote this part of the code when I was still the only contributor to the project, so nobody could review my work. We will now be trying to check every change, especially when it deals with critical parts of Plume, to avoid similar issues in the future, and we I'm really sorry this happened (even if I think nobody exploited it).

\n

Zanfib and stephenburgess8 also commited some small bugfixes, improving the general experience.

\n

New Features

\n

Let's now talk about the features that we introduced during this month.

\n

One of the most easy to spot is the redesign of Plume, made by @Madeorsk. I personaly love what he did, it really improved the readability and gave Plume a bit more of identity than the previous design. And he is still improving it.

\n

We also enabled Mardown in comment, to let you write more structured and nicely formatted responses.

\n

As you may have noticed, I have used mentions in this post. Indeed, it is now possible to mention someone in your articles or in comments. It works exactly the same way as in other apps, and you should receive a notification if someone mentionned you.

\n

A dashboard to manage your blogs has also been introduced. In the future it may be used to manage your drafts, and eventually to show some statistics. The goal is to have a more specific homepage for authors.

\n

The federation with other ActivityPub softwares, like Mastodon or Pleroma is starting to work quite well, but the federation between Plume instances is far from being complete. However, we started to work on it, and it is now possible to view a distant user profile or blog from your instance, even if only basic informations are fetched yet (the articles are not loaded for instance).

\n

Another new feature that may not be visible for everyone, is the new NodeInfo endpoint. NodeInfo is a protocol allowing to get informations about a specific federated instance (whatever software it runs). It means that Plume instances can now be listed on sites like fediverse.network.

\n

Maybe you wanted to host a Plume instance, but you don't like long install process during which you are just copy/pasting commands that you don't really understand from the documentation. That's why we introduced a setup script: the first you'll launch Plume, it will ask you a few questions and automatically setup your instance in a few minutes. We hope that this feature will help to host small instances, run by non-professional adminsys. You can see a demo of this tool on asciinema.

\n

Last but not least, Plume is now translatable! It is already available in English, French, Polish (thanks to @m4sk1n)) and German (thanks to bitkeks). If your browser is configured to display pages in these languages, you should normally see the interface in your language. And if your language is not present yet, feel free to add your translation.

\n

Other Changes

\n

We also improved the code a lot. We tried to separate each part as much as possible, making it easier to re-use for other projects. For instance, our database code is now isolated from the rest of the app, which means it will be easier to make import tools from other blogging engines. Some parts of the code are even shared with another project, Aardwolf a federated Facebook alternative. For instance, both of our projects use the same internationalization code, and once Aardwolf will implement federation, this part of the code will probably be shared too. Since the WebFinger module (used to find new users and blogs) and the CSRF protection code (see the \"Bug fixes and Security\" section) have been isolated in their own modules, they may be shared by both projects too.

\n

We also worked a lot on documentation. We now have articles explaining how to setup your Plume instance on various operating systems, but also documenting the translation process. I want to thank BanjoFox (who imported some documentation from their project, Aardwolf, as the setup is quite similar), Kushal and @gled@plume.mastodon.host for working on this.

\n

As you can see, there were many changes this month, but there still a lot to do. Your help will of course be welcome. If you want to contribute to the code, translate Plume in your language, write some documentation, or anything else (or even if you're just curious about the project), feel free to join our Matrix room: #plume:disroot.org. Otherwise, as BanjoFox said on the Aardwolf Team Mastodon account, talking about the project around you is one of the easiest way to help.

\n", + "id" : "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", + "likes" : null, + "name" : "This Month in Plume: June 2018", + "published" : "2018-07-10T20:16:24.087622Z", + "shares" : null, + "source" : null, + "tag" : [ + { + "href" : "https://baptiste.gelez.xyz/@/Trinity", + "name" : "@Trinity", + "type" : "Mention" + }, + { + "href" : "https://baptiste.gelez.xyz/@/kaniini/", + "name" : "@kaniini", + "type" : "Mention" + }, + { + "href" : "https://baptiste.gelez.xyz/@/Trinity", + "name" : "@Trinity", + "type" : "Mention" + } + ], + "to" : [ + "https://unixcorn.xyz/users/Bat", + "https://mastodon.host/users/federationbot", + "https://social.tcit.fr/users/tcit", + "https://framapiaf.org/users/qwerty", + "https://mastodon.social/users/lthms", + "https://eldritch.cafe/users/Nausicaa", + "https://imaginair.es/users/Elanndelh", + "https://framapiaf.org/users/Drulac", + "https://mastodon.partipirate.org/users/NicolasConstant", + "https://aleph.land/users/Madeorsk", + "https://maly.io/users/Troll", + "https://hostux.social/users/superjey", + "https://mamot.fr/users/Phigger", + "https://mastodon.social/users/wakest", + "https://social.coop/users/wakest", + "https://unixcorn.xyz/users/Ce_lo", + "https://social.art-software.fr/users/Electron", + "https://framapiaf.org/users/Quenti", + "https://toot.plus.yt/users/Djyp", + "https://mastodon.social/users/brainblasted", + "https://social.mochi.academy/users/Ambraven", + "https://social.hacktivis.me/users/lanodan", + "https://mastodon.eliotberriot.com/users/eliotberriot", + "https://edolas.world/users/0x1C3B00DA", + "https://toot.cafe/users/zack", + "https://manowar.social/users/zatnosk", + "https://eldritch.cafe/users/fluffy", + "https://mastodon.social/users/david_ross", + "https://kosmos.social/users/xiroux", + "https://mastodon.art/users/EmergencyBattle", + "https://mastodon.social/users/trwnh", + "https://octodon.social/users/pybyte", + "https://anticapitalist.party/users/Trinity", + "https://mstdn.mx/users/xavavu", + "https://baptiste.gelez.xyz/@/m4sk1n", + "https://eldritch.cafe/users/milia", + "https://mastodon.zaclys.com/users/arx", + "https://toot.cafe/users/sivy", + "https://mastodon.social/users/ortegacmanuel", + "https://mastodon.observer/users/stephen", + "https://octodon.social/users/chloe", + "https://unixcorn.xyz/users/AmauryPi", + "https://cybre.space/users/rick_777", + "https://mastodon.social/users/wezm", + "https://baptiste.gelez.xyz/@/idlesong", + "https://mamot.fr/users/dr4Ke", + "https://imaginair.es/users/Phigger", + "https://mamot.fr/users/dlink", + "https://anticapitalist.party/users/a000d4f7a91939d0e71df1646d7a48", + "https://framapiaf.org/users/PhieLaidMignon", + "https://mastodon.social/users/y6nH", + "https://crazynoisybizarre.town/users/FederationBot", + "https://social.weho.st/users/dvn", + "https://mastodon.art/users/Wolthera", + "https://diaspodon.fr/users/dada", + "https://pachyder.me/users/Lanza", + "https://mastodon.xyz/users/ag", + "https://aleph.land/users/yahananxie", + "https://mstdn.io/users/chablis_social", + "https://mastodon.gougere.fr/users/fabien", + "https://functional.cafe/users/otini", + "https://social.coop/users/bhaugen", + "https://octodon.social/users/donblanco", + "https://chaos.social/users/astro", + "https://pachyder.me/users/sibear", + "https://mamot.fr/users/yohann", + "https://social.wxcafe.net/users/Bat", + "https://mastodon.social/users/dansup", + "https://chaos.social/users/juh", + "https://scifi.fyi/users/paeneultima", + "https://hostux.social/users/Deuchnord", + "https://mstdn.fr/users/taziden", + "https://mamot.fr/users/PifyZ", + "https://mastodon.social/users/plantabaja", + "https://mastodon.social/users/gitzgrog", + "https://mastodon.social/users/Syluban", + "https://masto.pt/users/eloisa", + "https://pleroma.soykaf.com/users/notclacke", + "https://mastodon.social/users/SiegfriedEhret", + "https://writing.exchange/users/write_as", + "https://mstdn.io/users/shellkr", + "https://mastodon.uy/users/jorge", + "https://mastodon.technology/users/bobstechsite", + "https://mastodon.social/users/hinterwaeldler", + "https://mastodon.xyz/users/mgdelacroix", + "https://mastodon.cloud/users/jjatria", + "https://baptiste.gelez.xyz/@/Jade/", + "https://edolas.world/users/pfm", + "https://mstdn.io/users/jort", + "https://mastodon.social/users/andreipetcu", + "https://mastodon.technology/users/0xf00fc7c8", + "https://mastodon.social/users/khanate", + "https://mastodon.technology/users/francois", + "https://mastodon.social/users/glherrmann", + "https://mastodon.host/users/gled", + "https://social.holdmybeer.solutions/users/kemonine", + "https://scholar.social/users/bgcarlisle", + "https://mastodon.social/users/oldgun", + "https://baptiste.gelez.xyz/@/snoe/", + "https://mastodon.at/users/switchingsocial", + "https://scifi.fyi/users/BrokenBiscuit", + "https://dev.glitch.social/users/hoodie", + "https://todon.nl/users/paulfree14", + "https://mastodon.social/users/aadilayub", + "https://social.fsck.club/users/anarchosaurus", + "https://mastodonten.de/users/GiantG", + "https://mastodon.technology/users/cj", + "https://cybre.space/users/sam", + "https://layer8.space/users/silkevicious", + "https://mastodon.xyz/users/Jimmyrwx", + "https://fosstodon.org/users/danyspin97", + "https://mstdn.io/users/cristhyano", + "https://mastodon.social/users/vanyok", + "https://hulvr.com/users/rook", + "https://niu.moe/users/Lucifer", + "https://mamot.fr/users/Thibaut", + "https://mastodont.cat/users/bgta", + "https://mstdn.io/users/hontoni", + "https://niu.moe/users/lionirdeadman", + "https://functional.cafe/users/phoe", + "https://mastodon.social/users/toontoet", + "https://mastodon.social/users/danipozo", + "https://scholar.social/users/robertson", + "https://mastodon.social/users/aldatsa", + "https://elekk.xyz/users/maloki", + "https://kitty.town/users/nursemchurt", + "https://neigh.horse/users/commagray", + "https://mastodon.social/users/hirojin", + "https://mastodon.xyz/users/mareklach", + "https://chaos.social/users/benthor", + "https://mastodon.social/users/djperreault", + "https://mastodon.art/users/eylul", + "https://mastodon.opportunis.me/users/bob", + "https://tootplanet.space/users/Shutsumon", + "https://toot.cat/users/woozle", + "https://mastodon.social/users/StephenLB", + "https://sleeping.town/users/oct2pus", + "https://mastodon.indie.host/users/stragu", + "https://social.coop/users/gilscottfitzgerald", + "https://icosahedron.website/users/joeld", + "https://mastodon.social/users/hellion", + "https://cybre.space/users/cooler_ranch", + "https://mastodon.social/users/kelsonv", + "https://mastodon.lat/users/scalpol", + "https://writing.exchange/users/hnb", + "https://hex.bz/users/Horst", + "https://mastodon.social/users/weddle", + "https://maly.io/users/sonya", + "https://social.coop/users/medusa", + "https://mastodon.social/users/DystopianK", + "https://mstdn.io/users/d_io", + "https://fosstodon.org/users/brandon", + "https://fosstodon.org/users/Cando", + "https://mastodon.host/users/panina", + "https://floss.social/users/tuxether", + "https://social.tchncs.de/users/suitbertmonz", + "https://mastodon.social/users/jrt", + "https://mastodon.social/users/sirikon", + "https://mstdn.io/users/yabirgb", + "https://mastodon.cloud/users/FerdiZ", + "https://mastodon.social/users/carlchenet", + "https://social.polonkai.eu/users/calendar_social", + "https://social.polonkai.eu/users/gergely", + "https://mastodon.social/users/Jelv", + "https://mastodon.social/users/srinicame", + "https://cybre.space/users/mastoabed", + "https://mastodon.social/users/tagomago", + "https://lgbt.io/users/bootblackCub", + "https://niu.moe/users/Nopplyy", + "https://mastodon.social/users/bpugh", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" : "Article", + "uploadMedia" : null, + "url" : "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/" +} diff --git a/test/fixtures/tesla_mock/framatube.org-video.json b/test/fixtures/tesla_mock/framatube.org-video.json new file mode 100644 index 000000000..3d53f0c97 --- /dev/null +++ b/test/fixtures/tesla_mock/framatube.org-video.json @@ -0,0 +1 @@ +{"type":"Video","id":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206","name":"Déframasoftisons Internet [Framasoft]","duration":"PT3622S","uuid":"6050732a-8a7a-43d4-a6cd-809525a1d206","tag":[{"type":"Hashtag","name":"déframasoftisons"},{"type":"Hashtag","name":"EPN23"},{"type":"Hashtag","name":"framaconf"},{"type":"Hashtag","name":"Framasoft"},{"type":"Hashtag","name":"pyg"}],"category":{"identifier":"15","name":"Science & Technology"},"views":122,"sensitive":false,"waitTranscoding":false,"state":1,"commentsEnabled":true,"downloadEnabled":true,"published":"2020-05-24T18:34:31.569Z","originallyPublishedAt":"2019-11-30T23:00:00.000Z","updated":"2020-07-05T09:01:01.720Z","mediaType":"text/markdown","content":"Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?\r\n\r\nTranscription par @april...","support":null,"subtitleLanguage":[],"icon":{"type":"Image","url":"https://framatube.org/static/thumbnails/6050732a-8a7a-43d4-a6cd-809525a1d206.jpg","mediaType":"image/jpeg","width":223,"height":122},"url":[{"type":"Link","mediaType":"text/html","href":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080,"size":1157359410,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309939","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent&xt=urn:btih:381c9429900552e23a4eb506318f1fa01e4d63a8&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480,"size":250095131,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309941","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent&xt=urn:btih:a181dcbb5368ab5c31cc9ff07634becb72c344ee&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360,"size":171357733,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309942","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent&xt=urn:btih:aedfa9479ea04a175eee0b0bd0bda64076308746&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720,"size":497100839,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309943","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent&xt=urn:btih:71971668f82a3b24ac71bc3a982848dd8dc5a5f5&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240,"size":113038439,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309944","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent&xt=urn:btih:c42aa6c95efb28d9f114ebd98537f7b00fa72246&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240},{"type":"Link","mediaType":"application/x-mpegURL","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/master.m3u8","tag":[{"type":"Infohash","name":"f7428214539626e062f300f2ca4cf9154575144e"},{"type":"Infohash","name":"46e236dffb1ea6b9123a5396cbe88e97dd94cc6c"},{"type":"Infohash","name":"11f1045830b5d786c788f2594d19f128764e7d87"},{"type":"Infohash","name":"4327ad3e0d84de100130a27e9ab6fe40c4284f0e"},{"type":"Infohash","name":"41e2eee8e7b23a63c23a77c40a46de11492a4831"},{"type":"Link","name":"sha256","mediaType":"application/json","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/segments-sha256.json"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080,"size":1156777472,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309940","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent&xt=urn:btih:0204d780ebfab0d5d9d3476a038e812ad792deeb&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480,"size":249562889,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309945","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent&xt=urn:btih:5d14f38ded29de629668fe1cfc61a75f4cce2628&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360,"size":170836415,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309946","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent&xt=urn:btih:30125488789080ad405ebcee6c214945f31b8f30&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720,"size":496533741,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309947","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent&xt=urn:btih:8ed1e8bccde709901c26e315fc8f53bfd26d1ba6&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240,"size":112529249,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309948","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent&xt=urn:btih:8b452bf4e70b9078d4e74ca8b5523cc9dc70d10a&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240}]}],"likes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/likes","dislikes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/dislikes","shares":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/announces","comments":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/comments","attributedTo":[{"type":"Person","id":"https://framatube.org/accounts/framasoft"},{"type":"Group","id":"https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://framatube.org/accounts/framasoft/followers"],"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017"},{"pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","Infohash":"pt:Infohash","Playlist":"pt:Playlist","PlaylistElement":"pt:PlaylistElement","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"},"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}]} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json b/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json new file mode 100644 index 000000000..1c3f779b3 --- /dev/null +++ b/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json @@ -0,0 +1 @@ +{"type":"Person","id":"https://framatube.org/accounts/framasoft","following":"https://framatube.org/accounts/framasoft/following","followers":"https://framatube.org/accounts/framasoft/followers","playlists":"https://framatube.org/accounts/framasoft/playlists","inbox":"https://framatube.org/accounts/framasoft/inbox","outbox":"https://framatube.org/accounts/framasoft/outbox","preferredUsername":"framasoft","url":"https://framatube.org/accounts/framasoft","name":"Framasoft","endpoints":{"sharedInbox":"https://framatube.org/inbox"},"publicKey":{"id":"https://framatube.org/accounts/framasoft#main-key","owner":"https://framatube.org/accounts/framasoft","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuRh3frgIg866D0y0FThp\nSUkJImMcHGkUvpYQYv2iUgarZZtEbwT8PfQf0bJazy+cP8KqQmMDf5PBhT7dfdny\nf/GKGMw9Olc+QISeKDj3sqZ3Csrm4KV4avMGCfth6eSU7LozojeSGCXdUFz/8UgE\nfhV4mJjEX/FbwRYoKlagv5rY9mkX5XomzZU+z9j6ZVXyofwOwJvmI1hq0SYDv2bc\neB/RgIh/H0nyMtF8o+0CT42FNEET9j9m1BKOBtPzwZHmitKRkEmui5cK256s1laB\nT61KHpcD9gQKkQ+I3sFEzCBUJYfVo6fUe+GehBZuAfq4qDhd15SfE4K9veDscDFI\nTwIDAQAB\n-----END PUBLIC KEY-----"},"icon":{"type":"Image","mediaType":"image/png","url":"https://framatube.org/lazy-static/avatars/f73876f5-1d45-4f8a-942a-d3d5d5ac5dc1.png"},"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017","pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","expires":"sc:expires","CacheFile":"pt:CacheFile","Infohash":"pt:Infohash","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"}},{"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}],"summary":null} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com.html b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html new file mode 100644 index 000000000..880273d74 --- /dev/null +++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html @@ -0,0 +1,301 @@ + + + + Osada + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

Welcome to Osada

+ +
+
+
+ + + +
+
+ + +
+
+ +
+ +
+ +
+ +
+ Remote Authentication +
+ +
+ + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/test/fixtures/tesla_mock/peertube.moe-vid.json b/test/fixtures/tesla_mock/peertube.moe-vid.json index 76296eb7d..ceebb90b7 100644 --- a/test/fixtures/tesla_mock/peertube.moe-vid.json +++ b/test/fixtures/tesla_mock/peertube.moe-vid.json @@ -1 +1,187 @@ -{"type":"Video","id":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3","name":"Friday Night","duration":"PT29S","uuid":"df5f464b-be8d-46fb-ad81-2d4c2d1630e3","tag":[{"type":"Hashtag","name":"feels"}],"views":12,"sensitive":false,"commentsEnabled":true,"published":"2018-03-23T16:43:22.988Z","updated":"2018-03-24T16:28:46.002Z","mediaType":"text/markdown","content":"tfw\r\n\r\n\r\nsong is 'my old piano' by diana ross","support":null,"icon":{"type":"Image","url":"https://peertube.moe/static/thumbnails/df5f464b-be8d-46fb-ad81-2d4c2d1630e3.jpg","mediaType":"image/jpeg","width":200,"height":110},"url":[{"type":"Link","mimeType":"video/mp4","href":"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4","width":480,"size":5015880},{"type":"Link","mimeType":"application/x-bittorrent","href":"https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.torrent","width":480},{"type":"Link","mimeType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.torrent&xt=urn:btih:11d3af6b5c812a376c2b29cdbd46e5fb42ee730e&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4","width":480},{"type":"Link","mimeType":"video/mp4","href":"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.mp4","width":360,"size":3620040},{"type":"Link","mimeType":"application/x-bittorrent","href":"https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.torrent","width":360},{"type":"Link","mimeType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.torrent&xt=urn:btih:1c3885b4d7cdb46193b62b9b76e72b1409cfb297&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.mp4","width":360},{"type":"Link","mimeType":"video/mp4","href":"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.mp4","width":240,"size":2305488},{"type":"Link","mimeType":"application/x-bittorrent","href":"https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.torrent","width":240},{"type":"Link","mimeType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.torrent&xt=urn:btih:ac5773352d9e26f982d2da63acfb244f01ccafa4&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.mp4","width":240},{"type":"Link","mimeType":"video/mp4","href":"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.mp4","width":720,"size":7928231},{"type":"Link","mimeType":"application/x-bittorrent","href":"https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.torrent","width":720},{"type":"Link","mimeType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.torrent&xt=urn:btih:b591068f4533c4e2865bb4cbb89887aecccdc523&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.mp4","width":720},{"type":"Link","mimeType":"text/html","href":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"}],"likes":{"id":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/likes","type":"OrderedCollection","totalItems":0,"orderedItems":[]},"dislikes":{"id":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/dislikes","type":"OrderedCollection","totalItems":0,"orderedItems":[]},"shares":{"id":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces","type":"OrderedCollection","totalItems":2,"orderedItems":["https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces/465","https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces/1"]},"comments":{"id":"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/comments","type":"OrderedCollection","totalItems":0,"orderedItems":[]},"attributedTo":[{"type":"Group","id":"https://peertube.moe/video-channels/5224869f-aa63-4c83-ab3a-87c3a5ac440e"},{"type":"Person","id":"https://peertube.moe/accounts/7even"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017","Hashtag":"as:Hashtag","uuid":"http://schema.org/identifier","category":"http://schema.org/category","licence":"http://schema.org/license","sensitive":"as:sensitive","language":"http://schema.org/inLanguage","views":"http://schema.org/Number","size":"http://schema.org/Number","commentsEnabled":"http://schema.org/Boolean","support":"http://schema.org/Text"},{"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}]} \ No newline at end of file +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag" : "as:Hashtag", + "RsaSignature2017" : "https://w3id.org/security#RsaSignature2017", + "category" : "http://schema.org/category", + "commentsEnabled" : "http://schema.org/Boolean", + "language" : "http://schema.org/inLanguage", + "licence" : "http://schema.org/license", + "sensitive" : "as:sensitive", + "size" : "http://schema.org/Number", + "support" : "http://schema.org/Text", + "uuid" : "http://schema.org/identifier", + "views" : "http://schema.org/Number" + }, + { + "comments" : { + "@id" : "as:comments", + "@type" : "@id" + }, + "dislikes" : { + "@id" : "as:dislikes", + "@type" : "@id" + }, + "likes" : { + "@id" : "as:likes", + "@type" : "@id" + }, + "shares" : { + "@id" : "as:shares", + "@type" : "@id" + } + } + ], + "attributedTo" : [ + { + "id" : "https://peertube.moe/video-channels/5224869f-aa63-4c83-ab3a-87c3a5ac440e", + "type" : "Group" + }, + { + "id" : "https://peertube.moe/accounts/7even", + "type" : "Person" + } + ], + "cc" : [], + "comments" : { + "id" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/comments", + "orderedItems" : [], + "totalItems" : 0, + "type" : "OrderedCollection" + }, + "commentsEnabled" : true, + "content" : "tfw\r\n\r\n\r\nsong is 'my old piano' by diana ross", + "dislikes" : { + "id" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/dislikes", + "orderedItems" : [], + "totalItems" : 0, + "type" : "OrderedCollection" + }, + "duration" : "PT29S", + "icon" : { + "height" : 110, + "mediaType" : "image/jpeg", + "type" : "Image", + "url" : "https://peertube.moe/static/thumbnails/df5f464b-be8d-46fb-ad81-2d4c2d1630e3.jpg", + "width" : 200 + }, + "id" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", + "likes" : { + "id" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/likes", + "orderedItems" : [], + "totalItems" : 0, + "type" : "OrderedCollection" + }, + "mediaType" : "text/markdown", + "name" : "Friday Night", + "published" : "2018-03-23T16:43:22.988Z", + "sensitive" : false, + "shares" : { + "id" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces", + "orderedItems" : [ + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces/465", + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3/announces/1" + ], + "totalItems" : 2, + "type" : "OrderedCollection" + }, + "support" : null, + "tag" : [ + { + "name" : "feels", + "type" : "Hashtag" + } + ], + "to" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" : "Video", + "updated" : "2018-03-24T16:28:46.002Z", + "url" : [ + { + "href" : "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mimeType" : "video/mp4", + "size" : 5015880, + "type" : "Link", + "width" : 480 + }, + { + "href" : "https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.torrent", + "mimeType" : "application/x-bittorrent", + "type" : "Link", + "width" : 480 + }, + { + "href" : "magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.torrent&xt=urn:btih:11d3af6b5c812a376c2b29cdbd46e5fb42ee730e&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mimeType" : "application/x-bittorrent;x-scheme-handler/magnet", + "type" : "Link", + "width" : 480 + }, + { + "href" : "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.mp4", + "mimeType" : "video/mp4", + "size" : 3620040, + "type" : "Link", + "width" : 360 + }, + { + "href" : "https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.torrent", + "mimeType" : "application/x-bittorrent", + "type" : "Link", + "width" : 360 + }, + { + "href" : "magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.torrent&xt=urn:btih:1c3885b4d7cdb46193b62b9b76e72b1409cfb297&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-360.mp4", + "mimeType" : "application/x-bittorrent;x-scheme-handler/magnet", + "type" : "Link", + "width" : 360 + }, + { + "href" : "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.mp4", + "mimeType" : "video/mp4", + "size" : 2305488, + "type" : "Link", + "width" : 240 + }, + { + "href" : "https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.torrent", + "mimeType" : "application/x-bittorrent", + "type" : "Link", + "width" : 240 + }, + { + "href" : "magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.torrent&xt=urn:btih:ac5773352d9e26f982d2da63acfb244f01ccafa4&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-240.mp4", + "mimeType" : "application/x-bittorrent;x-scheme-handler/magnet", + "type" : "Link", + "width" : 240 + }, + { + "href" : "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.mp4", + "mimeType" : "video/mp4", + "size" : 7928231, + "type" : "Link", + "width" : 720 + }, + { + "href" : "https://peertube.moe/static/torrents/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.torrent", + "mimeType" : "application/x-bittorrent", + "type" : "Link", + "width" : 720 + }, + { + "href" : "magnet:?xs=https%3A%2F%2Fpeertube.moe%2Fstatic%2Ftorrents%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.torrent&xt=urn:btih:b591068f4533c4e2865bb4cbb89887aecccdc523&dn=Friday+Night&tr=wss%3A%2F%2Fpeertube.moe%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube.moe%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube.moe%2Fstatic%2Fwebseed%2Fdf5f464b-be8d-46fb-ad81-2d4c2d1630e3-720.mp4", + "mimeType" : "application/x-bittorrent;x-scheme-handler/magnet", + "type" : "Link", + "width" : 720 + }, + { + "href" : "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", + "mimeType" : "text/html", + "type" : "Link" + } + ], + "uuid" : "df5f464b-be8d-46fb-ad81-2d4c2d1630e3", + "views" : 12 +} diff --git a/test/gun/conneciton_pool_test.exs b/test/gun/conneciton_pool_test.exs new file mode 100644 index 000000000..aea908fac --- /dev/null +++ b/test/gun/conneciton_pool_test.exs @@ -0,0 +1,101 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Gun.ConnectionPoolTest do + use Pleroma.DataCase + + import Mox + import ExUnit.CaptureLog + alias Pleroma.Config + alias Pleroma.Gun.ConnectionPool + + defp gun_mock(_) do + Pleroma.GunMock + |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end) + |> stub(:await_up, fn _, _ -> {:ok, :http} end) + |> stub(:set_owner, fn _, _ -> :ok end) + + :ok + end + + setup :set_mox_from_context + setup :gun_mock + + test "gives the same connection to 2 concurrent requests" do + Enum.map( + [ + "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf", + "http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf" + ], + fn uri -> + uri = URI.parse(uri) + task_parent = self() + + Task.start_link(fn -> + {:ok, conn} = ConnectionPool.get_conn(uri, []) + ConnectionPool.release_conn(conn) + send(task_parent, conn) + end) + end + ) + + [pid, pid] = + for _ <- 1..2 do + receive do + pid -> pid + end + end + end + + test "connection limit is respected with concurrent requests" do + clear_config([:connections_pool, :max_connections]) do + Config.put([:connections_pool, :max_connections], 1) + # The supervisor needs a reboot to apply the new config setting + Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) + + on_exit(fn -> + Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill) + end) + end + + capture_log(fn -> + Enum.map( + [ + "https://ninenines.eu/", + "https://youtu.be/PFGwMiDJKNY" + ], + fn uri -> + uri = URI.parse(uri) + task_parent = self() + + Task.start_link(fn -> + result = ConnectionPool.get_conn(uri, []) + # Sleep so that we don't end up with a situation, + # where request from the second process gets processed + # only after the first process already released the connection + Process.sleep(50) + + case result do + {:ok, pid} -> + ConnectionPool.release_conn(pid) + + _ -> + nil + end + + send(task_parent, result) + end) + end + ) + + [{:error, :pool_full}, {:ok, _pid}] = + for _ <- 1..2 do + receive do + result -> result + end + end + |> Enum.sort() + end) + end +end diff --git a/test/html_test.exs b/test/html_test.exs index 0a4b4ebbc..f8907c8b4 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -237,5 +237,19 @@ test "does not crash when there is an HTML entity in a link" do assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) end + + test "skips attachment links" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: + "image.png" + }) + + object = Object.normalize(activity) + + assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"]) + end end end diff --git a/test/http/adapter_helper/gun_test.exs b/test/http/adapter_helper/gun_test.exs index 2e961826e..80589c73d 100644 --- a/test/http/adapter_helper/gun_test.exs +++ b/test/http/adapter_helper/gun_test.exs @@ -9,24 +9,10 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do import Mox alias Pleroma.Config - alias Pleroma.Gun.Conn alias Pleroma.HTTP.AdapterHelper.Gun - alias Pleroma.Pool.Connections setup :verify_on_exit! - defp gun_mock(_) do - gun_mock() - :ok - end - - defp gun_mock do - Pleroma.GunMock - |> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(1000) end) end) - |> stub(:await_up, fn _, _ -> {:ok, :http} end) - |> stub(:set_owner, fn _, _ -> :ok end) - end - describe "options/1" do setup do: clear_config([:http, :adapter], a: 1, b: 2) @@ -35,7 +21,6 @@ test "https url with default port" do opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] - assert opts[:tls_opts][:log_level] == :warning end test "https ipv4 with default port" do @@ -43,7 +28,6 @@ test "https ipv4 with default port" do opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] - assert opts[:tls_opts][:log_level] == :warning end test "https ipv6 with default port" do @@ -51,7 +35,6 @@ test "https ipv6 with default port" do opts = Gun.options([receive_conn: false], uri) assert opts[:certificates_verification] - assert opts[:tls_opts][:log_level] == :warning end test "https url with non standart port" do @@ -62,46 +45,12 @@ test "https url with non standart port" do assert opts[:certificates_verification] end - test "get conn on next request" do - gun_mock() - level = Application.get_env(:logger, :level) - Logger.configure(level: :debug) - on_exit(fn -> Logger.configure(level: level) end) - uri = URI.parse("http://some-domain2.com") - - opts = Gun.options(uri) - - assert opts[:conn] == nil - assert opts[:close_conn] == nil - - Process.sleep(50) - opts = Gun.options(uri) - - assert is_pid(opts[:conn]) - assert opts[:close_conn] == false - end - test "merges with defaul http adapter config" do defaults = Gun.options([receive_conn: false], URI.parse("https://example.com")) assert Keyword.has_key?(defaults, :a) assert Keyword.has_key?(defaults, :b) end - test "default ssl adapter opts with connection" do - gun_mock() - uri = URI.parse("https://some-domain.com") - - :ok = Conn.open(uri, :gun_connections) - - opts = Gun.options(uri) - - assert opts[:certificates_verification] - refute opts[:tls_opts] == [] - - assert opts[:close_conn] == false - assert is_pid(opts[:conn]) - end - test "parses string proxy host & port" do proxy = Config.get([:http, :proxy_url]) Config.put([:http, :proxy_url], "localhost:8123") @@ -132,127 +81,4 @@ test "passed opts have more weight than defaults" do assert opts[:proxy] == {'example.com', 4321} end end - - describe "options/1 with receive_conn parameter" do - setup :gun_mock - - test "receive conn by default" do - uri = URI.parse("http://another-domain.com") - :ok = Conn.open(uri, :gun_connections) - - received_opts = Gun.options(uri) - assert received_opts[:close_conn] == false - assert is_pid(received_opts[:conn]) - end - - test "don't receive conn if receive_conn is false" do - uri = URI.parse("http://another-domain.com") - :ok = Conn.open(uri, :gun_connections) - - opts = [receive_conn: false] - received_opts = Gun.options(opts, uri) - assert received_opts[:close_conn] == nil - assert received_opts[:conn] == nil - end - end - - describe "after_request/1" do - setup :gun_mock - - test "body_as not chunks" do - uri = URI.parse("http://some-domain.com") - :ok = Conn.open(uri, :gun_connections) - opts = Gun.options(uri) - :ok = Gun.after_request(opts) - conn = opts[:conn] - - assert %Connections{ - conns: %{ - "http:some-domain.com:80" => %Pleroma.Gun.Conn{ - conn: ^conn, - conn_state: :idle, - used_by: [] - } - } - } = Connections.get_state(:gun_connections) - end - - test "body_as chunks" do - uri = URI.parse("http://some-domain.com") - :ok = Conn.open(uri, :gun_connections) - opts = Gun.options([body_as: :chunks], uri) - :ok = Gun.after_request(opts) - conn = opts[:conn] - self = self() - - assert %Connections{ - conns: %{ - "http:some-domain.com:80" => %Pleroma.Gun.Conn{ - conn: ^conn, - conn_state: :active, - used_by: [{^self, _}] - } - } - } = Connections.get_state(:gun_connections) - end - - test "with no connection" do - uri = URI.parse("http://uniq-domain.com") - - :ok = Conn.open(uri, :gun_connections) - - opts = Gun.options([body_as: :chunks], uri) - conn = opts[:conn] - opts = Keyword.delete(opts, :conn) - self = self() - - :ok = Gun.after_request(opts) - - assert %Connections{ - conns: %{ - "http:uniq-domain.com:80" => %Pleroma.Gun.Conn{ - conn: ^conn, - conn_state: :active, - used_by: [{^self, _}] - } - } - } = Connections.get_state(:gun_connections) - end - - test "with ipv4" do - uri = URI.parse("http://127.0.0.1") - :ok = Conn.open(uri, :gun_connections) - opts = Gun.options(uri) - :ok = Gun.after_request(opts) - conn = opts[:conn] - - assert %Connections{ - conns: %{ - "http:127.0.0.1:80" => %Pleroma.Gun.Conn{ - conn: ^conn, - conn_state: :idle, - used_by: [] - } - } - } = Connections.get_state(:gun_connections) - end - - test "with ipv6" do - uri = URI.parse("http://[2a03:2880:f10c:83:face:b00c:0:25de]") - :ok = Conn.open(uri, :gun_connections) - opts = Gun.options(uri) - :ok = Gun.after_request(opts) - conn = opts[:conn] - - assert %Connections{ - conns: %{ - "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Pleroma.Gun.Conn{ - conn: ^conn, - conn_state: :idle, - used_by: [] - } - } - } = Connections.get_state(:gun_connections) - end - end end diff --git a/test/http/connection_test.exs b/test/http/connection_test.exs deleted file mode 100644 index 7c94a50b2..000000000 --- a/test/http/connection_test.exs +++ /dev/null @@ -1,135 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.HTTP.ConnectionTest do - use ExUnit.Case - use Pleroma.Tests.Helpers - - import ExUnit.CaptureLog - - alias Pleroma.Config - alias Pleroma.HTTP.Connection - - describe "parse_host/1" do - test "as atom to charlist" do - assert Connection.parse_host(:localhost) == 'localhost' - end - - test "as string to charlist" do - assert Connection.parse_host("localhost.com") == 'localhost.com' - end - - test "as string ip to tuple" do - assert Connection.parse_host("127.0.0.1") == {127, 0, 0, 1} - end - end - - describe "parse_proxy/1" do - test "ip with port" do - assert Connection.parse_proxy("127.0.0.1:8123") == {:ok, {127, 0, 0, 1}, 8123} - end - - test "host with port" do - assert Connection.parse_proxy("localhost:8123") == {:ok, 'localhost', 8123} - end - - test "as tuple" do - assert Connection.parse_proxy({:socks4, :localhost, 9050}) == - {:ok, :socks4, 'localhost', 9050} - end - - test "as tuple with string host" do - assert Connection.parse_proxy({:socks5, "localhost", 9050}) == - {:ok, :socks5, 'localhost', 9050} - end - end - - describe "parse_proxy/1 errors" do - test "ip without port" do - capture_log(fn -> - assert Connection.parse_proxy("127.0.0.1") == {:error, :invalid_proxy} - end) =~ "parsing proxy fail \"127.0.0.1\"" - end - - test "host without port" do - capture_log(fn -> - assert Connection.parse_proxy("localhost") == {:error, :invalid_proxy} - end) =~ "parsing proxy fail \"localhost\"" - end - - test "host with bad port" do - capture_log(fn -> - assert Connection.parse_proxy("localhost:port") == {:error, :invalid_proxy_port} - end) =~ "parsing port in proxy fail \"localhost:port\"" - end - - test "ip with bad port" do - capture_log(fn -> - assert Connection.parse_proxy("127.0.0.1:15.9") == {:error, :invalid_proxy_port} - end) =~ "parsing port in proxy fail \"127.0.0.1:15.9\"" - end - - test "as tuple without port" do - capture_log(fn -> - assert Connection.parse_proxy({:socks5, :localhost}) == {:error, :invalid_proxy} - end) =~ "parsing proxy fail {:socks5, :localhost}" - end - - test "with nil" do - assert Connection.parse_proxy(nil) == nil - end - end - - describe "options/3" do - setup do: clear_config([:http, :proxy_url]) - - test "without proxy_url in config" do - Config.delete([:http, :proxy_url]) - - opts = Connection.options(%URI{}) - refute Keyword.has_key?(opts, :proxy) - end - - test "parses string proxy host & port" do - Config.put([:http, :proxy_url], "localhost:8123") - - opts = Connection.options(%URI{}) - assert opts[:proxy] == {'localhost', 8123} - end - - test "parses tuple proxy scheme host and port" do - Config.put([:http, :proxy_url], {:socks, 'localhost', 1234}) - - opts = Connection.options(%URI{}) - assert opts[:proxy] == {:socks, 'localhost', 1234} - end - - test "passed opts have more weight than defaults" do - Config.put([:http, :proxy_url], {:socks5, 'localhost', 1234}) - - opts = Connection.options(%URI{}, proxy: {'example.com', 4321}) - - assert opts[:proxy] == {'example.com', 4321} - end - end - - describe "format_host/1" do - test "with domain" do - assert Connection.format_host("example.com") == 'example.com' - end - - test "with idna domain" do - assert Connection.format_host("ですexample.com") == 'xn--example-183fne.com' - end - - test "with ipv4" do - assert Connection.format_host("127.0.0.1") == '127.0.0.1' - end - - test "with ipv6" do - assert Connection.format_host("2a03:2880:f10c:83:face:b00c:0:25de") == - '2a03:2880:f10c:83:face:b00c:0:25de' - end - end -end diff --git a/test/notification_test.exs b/test/notification_test.exs index 526f43fab..8243cfd34 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -22,6 +22,16 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.Streamer describe "create_notifications" do + test "never returns nil" do + user = insert(:user) + other_user = insert(:user, %{invisible: true}) + + {:ok, activity} = CommonAPI.post(user, %{status: "yeah"}) + {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + refute {:ok, [nil]} == Notification.create_notifications(activity) + end + test "creates a notification for an emoji reaction" do user = insert(:user) other_user = insert(:user) @@ -236,49 +246,18 @@ test "it creates a notification for an activity from a muted thread" do assert Notification.create_notification(activity, muter) end - test "it disables notifications from followers" do - follower = insert(:user) - - followed = - insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false}) - - User.follow(follower, followed) - {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) - refute Notification.create_notification(activity, followed) - end - - test "it disables notifications from non-followers" do + test "it disables notifications from strangers" do follower = insert(:user) followed = insert(:user, - notification_settings: %Pleroma.User.NotificationSetting{non_followers: false} + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} ) {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) end - test "it disables notifications from people the user follows" do - follower = - insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false}) - - followed = insert(:user) - User.follow(follower, followed) - follower = Repo.get(User, follower.id) - {:ok, activity} = CommonAPI.post(followed, %{status: "hey @#{follower.nickname}"}) - refute Notification.create_notification(activity, follower) - end - - test "it disables notifications from people the user does not follow" do - follower = - insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false}) - - followed = insert(:user) - {:ok, activity} = CommonAPI.post(followed, %{status: "hey @#{follower.nickname}"}) - refute Notification.create_notification(activity, follower) - end - test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) @@ -314,6 +293,44 @@ test "it disables notifications from people who are invisible" do {:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"}) refute Notification.create_notification(status, user) end + + test "it doesn't create notifications if content matches with an irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: true) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + + assert {:ok, []} == Notification.create_notifications(status) + end + + test "it creates notifications if content matches with a not irreversible filter" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + insert(:filter, user: subscriber, phrase: "cofe", hide: false) + + {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification + end + + test "it creates notifications when someone likes user's status with a filtered word" do + user = insert(:user) + other_user = insert(:user) + insert(:filter, user: user, phrase: "tesla", hide: true) + + {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"}) + {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id) + + {:ok, [notification]} = Notification.create_notifications(activity_two) + + assert notification + end end describe "follow / follow_request notifications" do @@ -980,8 +997,13 @@ test "move activity generates a notification" do end describe "for_user" do - test "it returns notifications for muted user without notifications" do + setup do user = insert(:user) + + {:ok, %{user: user}} + end + + test "it returns notifications for muted user without notifications", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted, false) @@ -992,8 +1014,7 @@ test "it returns notifications for muted user without notifications" do assert notification.activity.object end - test "it doesn't return notifications for muted user with notifications" do - user = insert(:user) + test "it doesn't return notifications for muted user with notifications", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted) @@ -1002,8 +1023,7 @@ test "it doesn't return notifications for muted user with notifications" do assert Notification.for_user(user) == [] end - test "it doesn't return notifications for blocked user" do - user = insert(:user) + test "it doesn't return notifications for blocked user", %{user: user} do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) @@ -1012,8 +1032,7 @@ test "it doesn't return notifications for blocked user" do assert Notification.for_user(user) == [] end - test "it doesn't return notifications for domain-blocked non-followed user" do - user = insert(:user) + test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") @@ -1034,8 +1053,7 @@ test "it returns notifications for domain-blocked but followed user" do assert length(Notification.for_user(user)) == 1 end - test "it doesn't return notifications for muted thread" do - user = insert(:user) + test "it doesn't return notifications for muted thread", %{user: user} do another_user = insert(:user) {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) @@ -1044,8 +1062,7 @@ test "it doesn't return notifications for muted thread" do assert Notification.for_user(user) == [] end - test "it returns notifications from a muted user when with_muted is set" do - user = insert(:user) + test "it returns notifications from a muted user when with_muted is set", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted) @@ -1054,8 +1071,9 @@ test "it returns notifications from a muted user when with_muted is set" do assert length(Notification.for_user(user, %{with_muted: true})) == 1 end - test "it doesn't return notifications from a blocked user when with_muted is set" do - user = insert(:user) + test "it doesn't return notifications from a blocked user when with_muted is set", %{ + user: user + } do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) @@ -1065,8 +1083,8 @@ test "it doesn't return notifications from a blocked user when with_muted is set end test "when with_muted is set, " <> - "it doesn't return notifications from a domain-blocked non-followed user" do - user = insert(:user) + "it doesn't return notifications from a domain-blocked non-followed user", + %{user: user} do blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") @@ -1075,8 +1093,7 @@ test "when with_muted is set, " <> assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) end - test "it returns notifications from muted threads when with_muted is set" do - user = insert(:user) + test "it returns notifications from muted threads when with_muted is set", %{user: user} do another_user = insert(:user) {:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"}) @@ -1084,5 +1101,33 @@ test "it returns notifications from muted threads when with_muted is set" do {:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"]) assert length(Notification.for_user(user, %{with_muted: true})) == 1 end + + test "it doesn't return notifications about mentions with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"}) + + assert Enum.empty?(Notification.for_user(user)) + end + + test "it returns notifications about mentions with not hidden filtered word", %{user: user} do + insert(:filter, user: user, phrase: "test", hide: false) + another_user = insert(:user) + + {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"}) + + assert length(Notification.for_user(user)) == 1 + end + + test "it returns notifications about favorites with filtered word", %{user: user} do + insert(:filter, user: user, phrase: "cofe", hide: true) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"}) + {:ok, _} = CommonAPI.favorite(another_user, activity.id) + + assert length(Notification.for_user(user)) == 1 + end end end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index c06e91f12..d9098ea1b 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -26,6 +26,46 @@ defmodule Pleroma.Object.FetcherTest do :ok end + describe "error cases" do + setup do + mock(fn + %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json") + } + + %{method: :get, url: "https://social.sakamoto.gq/users/eal"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/eal.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json") + } + + %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} -> + %Tesla.Env{ + status: 500 + } + end) + + :ok + end + + @tag capture_log: true + test "it works when fetching the OP actor errors out" do + # Here we simulate a case where the author of the OP can't be read + assert {:ok, _} = + Fetcher.fetch_object_from_id( + "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM" + ) + end + end + describe "max thread distance restriction" do @ap_id "http://mastodon.example.org/@admin/99541947525187367" setup do: clear_config([:instance, :federation_incoming_replies_max_depth]) diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs index 100016c62..89df03c4b 100644 --- a/test/plugs/admin_secret_authentication_plug_test.exs +++ b/test/plugs/admin_secret_authentication_plug_test.exs @@ -4,9 +4,14 @@ defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true + + import Mock import Pleroma.Factory alias Pleroma.Plugs.AdminSecretAuthenticationPlug + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Plugs.PlugHelper + alias Pleroma.Plugs.RateLimiter test "does nothing if a user is assigned", %{conn: conn} do user = insert(:user) @@ -25,6 +30,10 @@ test "does nothing if a user is assigned", %{conn: conn} do describe "when secret set it assigns an admin user" do setup do: clear_config([:admin_token]) + setup_with_mocks([{RateLimiter, [:passthrough], []}]) do + :ok + end + test "with `admin_token` query parameter", %{conn: conn} do Pleroma.Config.put(:admin_token, "password123") @@ -33,12 +42,14 @@ test "with `admin_token` query parameter", %{conn: conn} do |> AdminSecretAuthenticationPlug.call(%{}) refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) conn = %{conn | params: %{"admin_token" => "password123"}} |> AdminSecretAuthenticationPlug.call(%{}) assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) end test "with `x-admin-token` HTTP header", %{conn: conn} do @@ -50,6 +61,7 @@ test "with `x-admin-token` HTTP header", %{conn: conn} do |> AdminSecretAuthenticationPlug.call(%{}) refute conn.assigns[:user] + assert called(RateLimiter.call(conn, name: :authentication)) conn = conn @@ -57,6 +69,7 @@ test "with `x-admin-token` HTTP header", %{conn: conn} do |> AdminSecretAuthenticationPlug.call(%{}) assert conn.assigns[:user].is_admin + assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) end end end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 63b4d3f31..2297e3dac 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -4,17 +4,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do use Pleroma.Web.ConnCase + alias Pleroma.Config alias Plug.Conn - setup do: clear_config([:http_securiy, :enabled]) - setup do: clear_config([:http_security, :sts]) - setup do: clear_config([:http_security, :referrer_policy]) - describe "http security enabled" do - setup do - Config.put([:http_security, :enabled], true) - end + setup do: clear_config([:http_security, :enabled], true) test "it sends CSP headers when enabled", %{conn: conn} do conn = get(conn, "/api/v1/instance") @@ -29,7 +24,7 @@ test "it sends CSP headers when enabled", %{conn: conn} do end test "it sends STS headers when enabled", %{conn: conn} do - Config.put([:http_security, :sts], true) + clear_config([:http_security, :sts], true) conn = get(conn, "/api/v1/instance") @@ -38,7 +33,7 @@ test "it sends STS headers when enabled", %{conn: conn} do end test "it does not send STS headers when disabled", %{conn: conn} do - Config.put([:http_security, :sts], false) + clear_config([:http_security, :sts], false) conn = get(conn, "/api/v1/instance") @@ -47,23 +42,19 @@ test "it does not send STS headers when disabled", %{conn: conn} do end test "referrer-policy header reflects configured value", %{conn: conn} do - conn = get(conn, "/api/v1/instance") + resp = get(conn, "/api/v1/instance") - assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] + assert Conn.get_resp_header(resp, "referrer-policy") == ["same-origin"] - Config.put([:http_security, :referrer_policy], "no-referrer") + clear_config([:http_security, :referrer_policy], "no-referrer") - conn = - build_conn() - |> get("/api/v1/instance") + resp = get(conn, "/api/v1/instance") - assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] + assert Conn.get_resp_header(resp, "referrer-policy") == ["no-referrer"] end - test "it sends `report-to` & `report-uri` CSP response headers" do - conn = - build_conn() - |> get("/api/v1/instance") + test "it sends `report-to` & `report-uri` CSP response headers", %{conn: conn} do + conn = get(conn, "/api/v1/instance") [csp] = Conn.get_resp_header(conn, "content-security-policy") @@ -74,10 +65,67 @@ test "it sends `report-to` & `report-uri` CSP response headers" do assert reply_to == "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" end + + test "default values for img-src and media-src with disabled media proxy", %{conn: conn} do + conn = get(conn, "/api/v1/instance") + + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' https:;" + assert csp =~ "img-src 'self' data: blob: https:;" + end + end + + describe "img-src and media-src" do + setup do + clear_config([:http_security, :enabled], true) + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :proxy_opts, :redirect_on_failure], false) + end + + test "media_proxy with base_url", %{conn: conn} do + url = "https://example.com" + clear_config([:media_proxy, :base_url], url) + assert_media_img_src(conn, url) + end + + test "upload with base url", %{conn: conn} do + url = "https://example2.com" + clear_config([Pleroma.Upload, :base_url], url) + assert_media_img_src(conn, url) + end + + test "with S3 public endpoint", %{conn: conn} do + url = "https://example3.com" + clear_config([Pleroma.Uploaders.S3, :public_endpoint], url) + assert_media_img_src(conn, url) + end + + test "with captcha endpoint", %{conn: conn} do + clear_config([Pleroma.Captcha.Mock, :endpoint], "https://captcha.com") + assert_media_img_src(conn, "https://captcha.com") + end + + test "with media_proxy whitelist", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"]) + assert_media_img_src(conn, "https://example7.com https://example6.com") + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do + clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"]) + assert_media_img_src(conn, "example5.com example4.com") + end + end + + defp assert_media_img_src(conn, url) do + conn = get(conn, "/api/v1/instance") + [csp] = Conn.get_resp_header(conn, "content-security-policy") + assert csp =~ "media-src 'self' #{url};" + assert csp =~ "img-src 'self' data: blob: #{url};" end test "it does not send CSP headers when disabled", %{conn: conn} do - Config.put([:http_security, :enabled], false) + clear_config([:http_security, :enabled], false) conn = get(conn, "/api/v1/instance") diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs index b8f070d6a..be2613ad0 100644 --- a/test/plugs/instance_static_test.exs +++ b/test/plugs/instance_static_test.exs @@ -16,7 +16,7 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do test "overrides index" do bundled_index = get(build_conn(), "/") - assert html_response(bundled_index, 200) == File.read!("priv/static/index.html") + refute html_response(bundled_index, 200) == "hello world" File.write!(@dir <> "/index.html", "hello world") diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index fd6a50e53..8bc00e444 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -8,112 +8,30 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do alias Pleroma.Plugs.UserIsAdminPlug import Pleroma.Factory - describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + test "accepts a user that is an admin" do + user = insert(:user, is_admin: true) - test "accepts a user that is an admin" do - user = insert(:user, is_admin: true) + conn = assign(build_conn(), :user, user) - conn = assign(build_conn(), :user, user) + ret_conn = UserIsAdminPlug.call(conn, %{}) - ret_conn = UserIsAdminPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "denies a user that isn't an admin" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - - test "denies when a user isn't set" do - conn = UserIsAdminPlug.call(build_conn(), %{}) - - assert conn.status == 403 - end + assert conn == ret_conn end - describe "with [:auth, :enforce_oauth_admin_scope_usage]," do - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) + test "denies a user that isn't an admin" do + user = insert(:user) - setup do - admin_user = insert(:user, is_admin: true) - non_admin_user = insert(:user, is_admin: false) - blank_user = nil + conn = + build_conn() + |> assign(:user, user) + |> UserIsAdminPlug.call(%{}) - {:ok, %{users: [admin_user, non_admin_user, blank_user]}} - end + assert conn.status == 403 + end - test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do - user = insert(:user, is_admin: true) - token = insert(:oauth_token, user: user, scopes: ["admin:something"]) + test "denies when a user isn't set" do + conn = UserIsAdminPlug.call(build_conn(), %{}) - conn = - conn - |> assign(:user, user) - |> assign(:token, token) - - ret_conn = UserIsAdminPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do - user = insert(:user, is_admin: false) - token = insert(:oauth_token, user: user, scopes: ["admin:something"]) - - conn = - conn - |> assign(:user, user) - |> assign(:token, token) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - - test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do - token = insert(:oauth_token, scopes: ["admin:something"]) - - conn = - conn - |> assign(:user, nil) - |> assign(:token, token) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - - test "if token lacks admin scopes, denies users regardless of is_admin flag", - %{users: users} do - for user <- users do - token = insert(:oauth_token, user: user) - - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, token) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - end - - test "if token is missing, denies users regardless of is_admin flag", %{users: users} do - for user <- users do - conn = - build_conn() - |> assign(:user, user) - |> assign(:token, nil) - |> UserIsAdminPlug.call(%{}) - - assert conn.status == 403 - end - end + assert conn.status == 403 end end diff --git a/test/pool/connections_test.exs b/test/pool/connections_test.exs deleted file mode 100644 index aeda54875..000000000 --- a/test/pool/connections_test.exs +++ /dev/null @@ -1,760 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Pool.ConnectionsTest do - use ExUnit.Case, async: true - use Pleroma.Tests.Helpers - - import ExUnit.CaptureLog - import Mox - - alias Pleroma.Gun.Conn - alias Pleroma.GunMock - alias Pleroma.Pool.Connections - - setup :verify_on_exit! - - setup_all do - name = :test_connections - {:ok, pid} = Connections.start_link({name, [checkin_timeout: 150]}) - {:ok, _} = Registry.start_link(keys: :unique, name: Pleroma.GunMock) - - on_exit(fn -> - if Process.alive?(pid), do: GenServer.stop(name) - end) - - {:ok, name: name} - end - - defp open_mock(num \\ 1) do - GunMock - |> expect(:open, num, &start_and_register(&1, &2, &3)) - |> expect(:await_up, num, fn _, _ -> {:ok, :http} end) - |> expect(:set_owner, num, fn _, _ -> :ok end) - end - - defp connect_mock(mock) do - mock - |> expect(:connect, &connect(&1, &2)) - |> expect(:await, &await(&1, &2)) - end - - defp info_mock(mock), do: expect(mock, :info, &info(&1)) - - defp start_and_register('gun-not-up.com', _, _), do: {:error, :timeout} - - defp start_and_register(host, port, _) do - {:ok, pid} = Task.start_link(fn -> Process.sleep(1000) end) - - scheme = - case port do - 443 -> "https" - _ -> "http" - end - - Registry.register(GunMock, pid, %{ - origin_scheme: scheme, - origin_host: host, - origin_port: port - }) - - {:ok, pid} - end - - defp info(pid) do - [{_, info}] = Registry.lookup(GunMock, pid) - info - end - - defp connect(pid, _) do - ref = make_ref() - Registry.register(GunMock, ref, pid) - ref - end - - defp await(pid, ref) do - [{_, ^pid}] = Registry.lookup(GunMock, ref) - {:response, :fin, 200, []} - end - - defp now, do: :os.system_time(:second) - - describe "alive?/2" do - test "is alive", %{name: name} do - assert Connections.alive?(name) - end - - test "returns false if not started" do - refute Connections.alive?(:some_random_name) - end - end - - test "opens connection and reuse it on next request", %{name: name} do - open_mock() - url = "http://some-domain.com" - key = "http:some-domain.com:80" - refute Connections.checkin(url, name) - :ok = Conn.open(url, name) - - conn = Connections.checkin(url, name) - assert is_pid(conn) - assert Process.alive?(conn) - - self = self() - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert conn == reused_conn - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}, {^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - :ok = Connections.checkout(conn, self, name) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - :ok = Connections.checkout(conn, self, name) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [], - conn_state: :idle - } - } - } = Connections.get_state(name) - end - - test "reuse connection for idna domains", %{name: name} do - open_mock() - url = "http://ですsome-domain.com" - refute Connections.checkin(url, name) - - :ok = Conn.open(url, name) - - conn = Connections.checkin(url, name) - assert is_pid(conn) - assert Process.alive?(conn) - - self = self() - - %Connections{ - conns: %{ - "http:ですsome-domain.com:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert conn == reused_conn - end - - test "reuse for ipv4", %{name: name} do - open_mock() - url = "http://127.0.0.1" - - refute Connections.checkin(url, name) - - :ok = Conn.open(url, name) - - conn = Connections.checkin(url, name) - assert is_pid(conn) - assert Process.alive?(conn) - - self = self() - - %Connections{ - conns: %{ - "http:127.0.0.1:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert conn == reused_conn - - :ok = Connections.checkout(conn, self, name) - :ok = Connections.checkout(reused_conn, self, name) - - %Connections{ - conns: %{ - "http:127.0.0.1:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [], - conn_state: :idle - } - } - } = Connections.get_state(name) - end - - test "reuse for ipv6", %{name: name} do - open_mock() - url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]" - - refute Connections.checkin(url, name) - - :ok = Conn.open(url, name) - - conn = Connections.checkin(url, name) - assert is_pid(conn) - assert Process.alive?(conn) - - self = self() - - %Connections{ - conns: %{ - "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert conn == reused_conn - end - - test "up and down ipv4", %{name: name} do - open_mock() - |> info_mock() - |> allow(self(), name) - - self = self() - url = "http://127.0.0.1" - :ok = Conn.open(url, name) - conn = Connections.checkin(url, name) - send(name, {:gun_down, conn, nil, nil, nil}) - send(name, {:gun_up, conn, nil}) - - %Connections{ - conns: %{ - "http:127.0.0.1:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - end - - test "up and down ipv6", %{name: name} do - self = self() - - open_mock() - |> info_mock() - |> allow(self, name) - - url = "http://[2a03:2880:f10c:83:face:b00c:0:25de]" - :ok = Conn.open(url, name) - conn = Connections.checkin(url, name) - send(name, {:gun_down, conn, nil, nil, nil}) - send(name, {:gun_up, conn, nil}) - - %Connections{ - conns: %{ - "http:2a03:2880:f10c:83:face:b00c:0:25de:80" => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}], - conn_state: :active - } - } - } = Connections.get_state(name) - end - - test "reuses connection based on protocol", %{name: name} do - open_mock(2) - http_url = "http://some-domain.com" - http_key = "http:some-domain.com:80" - https_url = "https://some-domain.com" - https_key = "https:some-domain.com:443" - - refute Connections.checkin(http_url, name) - :ok = Conn.open(http_url, name) - conn = Connections.checkin(http_url, name) - assert is_pid(conn) - assert Process.alive?(conn) - - refute Connections.checkin(https_url, name) - :ok = Conn.open(https_url, name) - https_conn = Connections.checkin(https_url, name) - - refute conn == https_conn - - reused_https = Connections.checkin(https_url, name) - - refute conn == reused_https - - assert reused_https == https_conn - - %Connections{ - conns: %{ - ^http_key => %Conn{ - conn: ^conn, - gun_state: :up - }, - ^https_key => %Conn{ - conn: ^https_conn, - gun_state: :up - } - } - } = Connections.get_state(name) - end - - test "connection can't get up", %{name: name} do - expect(GunMock, :open, &start_and_register(&1, &2, &3)) - url = "http://gun-not-up.com" - - assert capture_log(fn -> - refute Conn.open(url, name) - refute Connections.checkin(url, name) - end) =~ - "Opening connection to http://gun-not-up.com failed with error {:error, :timeout}" - end - - test "process gun_down message and then gun_up", %{name: name} do - self = self() - - open_mock() - |> info_mock() - |> allow(self, name) - - url = "http://gun-down-and-up.com" - key = "http:gun-down-and-up.com:80" - :ok = Conn.open(url, name) - conn = Connections.checkin(url, name) - - assert is_pid(conn) - assert Process.alive?(conn) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up, - used_by: [{^self, _}] - } - } - } = Connections.get_state(name) - - send(name, {:gun_down, conn, :http, nil, nil}) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :down, - used_by: [{^self, _}] - } - } - } = Connections.get_state(name) - - send(name, {:gun_up, conn, :http}) - - conn2 = Connections.checkin(url, name) - assert conn == conn2 - - assert is_pid(conn2) - assert Process.alive?(conn2) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: _, - gun_state: :up, - used_by: [{^self, _}, {^self, _}] - } - } - } = Connections.get_state(name) - end - - test "async processes get same conn for same domain", %{name: name} do - open_mock() - url = "http://some-domain.com" - :ok = Conn.open(url, name) - - tasks = - for _ <- 1..5 do - Task.async(fn -> - Connections.checkin(url, name) - end) - end - - tasks_with_results = Task.yield_many(tasks) - - results = - Enum.map(tasks_with_results, fn {task, res} -> - res || Task.shutdown(task, :brutal_kill) - end) - - conns = for {:ok, value} <- results, do: value - - %Connections{ - conns: %{ - "http:some-domain.com:80" => %Conn{ - conn: conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - assert Enum.all?(conns, fn res -> res == conn end) - end - - test "remove frequently used and idle", %{name: name} do - open_mock(3) - self = self() - http_url = "http://some-domain.com" - https_url = "https://some-domain.com" - :ok = Conn.open(https_url, name) - :ok = Conn.open(http_url, name) - - conn1 = Connections.checkin(https_url, name) - - [conn2 | _conns] = - for _ <- 1..4 do - Connections.checkin(http_url, name) - end - - http_key = "http:some-domain.com:80" - - %Connections{ - conns: %{ - ^http_key => %Conn{ - conn: ^conn2, - gun_state: :up, - conn_state: :active, - used_by: [{^self, _}, {^self, _}, {^self, _}, {^self, _}] - }, - "https:some-domain.com:443" => %Conn{ - conn: ^conn1, - gun_state: :up, - conn_state: :active, - used_by: [{^self, _}] - } - } - } = Connections.get_state(name) - - :ok = Connections.checkout(conn1, self, name) - - another_url = "http://another-domain.com" - :ok = Conn.open(another_url, name) - conn = Connections.checkin(another_url, name) - - %Connections{ - conns: %{ - "http:another-domain.com:80" => %Conn{ - conn: ^conn, - gun_state: :up - }, - ^http_key => %Conn{ - conn: _, - gun_state: :up - } - } - } = Connections.get_state(name) - end - - describe "with proxy" do - test "as ip", %{name: name} do - open_mock() - |> connect_mock() - - url = "http://proxy-string.com" - key = "http:proxy-string.com:80" - :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) - - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - ^key => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - - test "as host", %{name: name} do - open_mock() - |> connect_mock() - - url = "http://proxy-tuple-atom.com" - :ok = Conn.open(url, name, proxy: {'localhost', 9050}) - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - "http:proxy-tuple-atom.com:80" => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - - test "as ip and ssl", %{name: name} do - open_mock() - |> connect_mock() - - url = "https://proxy-string.com" - - :ok = Conn.open(url, name, proxy: {{127, 0, 0, 1}, 8123}) - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - "https:proxy-string.com:443" => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - - test "as host and ssl", %{name: name} do - open_mock() - |> connect_mock() - - url = "https://proxy-tuple-atom.com" - :ok = Conn.open(url, name, proxy: {'localhost', 9050}) - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - "https:proxy-tuple-atom.com:443" => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - - test "with socks type", %{name: name} do - open_mock() - - url = "http://proxy-socks.com" - - :ok = Conn.open(url, name, proxy: {:socks5, 'localhost', 1234}) - - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - "http:proxy-socks.com:80" => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - - test "with socks4 type and ssl", %{name: name} do - open_mock() - url = "https://proxy-socks.com" - - :ok = Conn.open(url, name, proxy: {:socks4, 'localhost', 1234}) - - conn = Connections.checkin(url, name) - - %Connections{ - conns: %{ - "https:proxy-socks.com:443" => %Conn{ - conn: ^conn, - gun_state: :up - } - } - } = Connections.get_state(name) - - reused_conn = Connections.checkin(url, name) - - assert reused_conn == conn - end - end - - describe "crf/3" do - setup do - crf = Connections.crf(1, 10, 1) - {:ok, crf: crf} - end - - test "more used will have crf higher", %{crf: crf} do - # used 3 times - crf1 = Connections.crf(1, 10, crf) - crf1 = Connections.crf(1, 10, crf1) - - # used 2 times - crf2 = Connections.crf(1, 10, crf) - - assert crf1 > crf2 - end - - test "recently used will have crf higher on equal references", %{crf: crf} do - # used 3 sec ago - crf1 = Connections.crf(3, 10, crf) - - # used 4 sec ago - crf2 = Connections.crf(4, 10, crf) - - assert crf1 > crf2 - end - - test "equal crf on equal reference and time", %{crf: crf} do - # used 2 times - crf1 = Connections.crf(1, 10, crf) - - # used 2 times - crf2 = Connections.crf(1, 10, crf) - - assert crf1 == crf2 - end - - test "recently used will have higher crf", %{crf: crf} do - crf1 = Connections.crf(2, 10, crf) - crf1 = Connections.crf(1, 10, crf1) - - crf2 = Connections.crf(3, 10, crf) - crf2 = Connections.crf(4, 10, crf2) - assert crf1 > crf2 - end - end - - describe "get_unused_conns/1" do - test "crf is equalent, sorting by reference", %{name: name} do - Connections.add_conn(name, "1", %Conn{ - conn_state: :idle, - last_reference: now() - 1 - }) - - Connections.add_conn(name, "2", %Conn{ - conn_state: :idle, - last_reference: now() - }) - - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) - end - - test "reference is equalent, sorting by crf", %{name: name} do - Connections.add_conn(name, "1", %Conn{ - conn_state: :idle, - crf: 1.999 - }) - - Connections.add_conn(name, "2", %Conn{ - conn_state: :idle, - crf: 2 - }) - - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) - end - - test "higher crf and lower reference", %{name: name} do - Connections.add_conn(name, "1", %Conn{ - conn_state: :idle, - crf: 3, - last_reference: now() - 1 - }) - - Connections.add_conn(name, "2", %Conn{ - conn_state: :idle, - crf: 2, - last_reference: now() - }) - - assert [{"2", _unused_conn} | _others] = Connections.get_unused_conns(name) - end - - test "lower crf and lower reference", %{name: name} do - Connections.add_conn(name, "1", %Conn{ - conn_state: :idle, - crf: 1.99, - last_reference: now() - 1 - }) - - Connections.add_conn(name, "2", %Conn{ - conn_state: :idle, - crf: 2, - last_reference: now() - }) - - assert [{"1", _unused_conn} | _others] = Connections.get_unused_conns(name) - end - end - - test "count/1" do - name = :test_count - {:ok, _} = Connections.start_link({name, [checkin_timeout: 150]}) - assert Connections.count(name) == 0 - Connections.add_conn(name, "1", %Conn{conn: self()}) - assert Connections.count(name) == 1 - Connections.remove_conn(name, "1") - assert Connections.count(name) == 0 - end -end diff --git a/test/reverse_proxy/reverse_proxy_test.exs b/test/reverse_proxy/reverse_proxy_test.exs index c677066b3..8df63de65 100644 --- a/test/reverse_proxy/reverse_proxy_test.exs +++ b/test/reverse_proxy/reverse_proxy_test.exs @@ -314,7 +314,7 @@ defp disposition_headers_mock(headers) do test "not atachment", %{conn: conn} do disposition_headers_mock([ {"content-type", "image/gif"}, - {"content-length", 0} + {"content-length", "0"} ]) conn = ReverseProxy.call(conn, "/disposition") @@ -325,7 +325,7 @@ test "not atachment", %{conn: conn} do test "with content-disposition header", %{conn: conn} do disposition_headers_mock([ {"content-disposition", "attachment; filename=\"filename.jpg\""}, - {"content-length", 0} + {"content-length", "0"} ]) conn = ReverseProxy.call(conn, "/disposition") diff --git a/test/support/cluster.ex b/test/support/cluster.ex index deb37f361..524194cf4 100644 --- a/test/support/cluster.ex +++ b/test/support/cluster.ex @@ -97,7 +97,7 @@ def spawn_cluster(node_configs) do silence_logger_warnings(fn -> node_configs |> Enum.map(&Task.async(fn -> start_slave(&1) end)) - |> Enum.map(&Task.await(&1, 60_000)) + |> Enum.map(&Task.await(&1, 90_000)) end) end diff --git a/test/support/factory.ex b/test/support/factory.ex index 6e22b66a4..635d83650 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -67,6 +67,7 @@ def note_factory(attrs \\ %{}) do data = %{ "type" => "Note", "content" => text, + "source" => text, "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), "actor" => user.ap_id, "to" => ["https://www.w3.org/ns/activitystreams#Public"], @@ -427,4 +428,12 @@ def mfa_token_factory do user: build(:user) } end + + def filter_factory do + %Pleroma.Filter{ + user: build(:user), + filter_id: sequence(:filter_id, & &1), + phrase: "cofe" + } + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 3d5128835..19a202654 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -308,6 +308,22 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" }} end + def get("https://framatube.org/accounts/framasoft", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json") + }} + end + + def get("https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json") + }} + end + def get("https://peertube.social/accounts/craigmaloney", _, _, _) do {:ok, %Tesla.Env{ @@ -1326,6 +1342,18 @@ def get("https://relay.mastodon.host/actor", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}} end + def get("http://localhost:4001/", _, "", Accept: "text/html") do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} + end + + def get("https://osada.macgirvin.com/", _, "", Accept: "text/html") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com.html") + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ diff --git a/test/support/oban_helpers.ex b/test/support/oban_helpers.ex index e96994c57..9f90a821c 100644 --- a/test/support/oban_helpers.ex +++ b/test/support/oban_helpers.ex @@ -20,7 +20,7 @@ def perform_all do end def perform(%Oban.Job{} = job) do - res = apply(String.to_existing_atom("Elixir." <> job.worker), :perform, [job.args, job]) + res = apply(String.to_existing_atom("Elixir." <> job.worker), :perform, [job]) Repo.delete(job) res end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index a8ba0658d..79ab72002 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -10,6 +10,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do alias Pleroma.Web.ActivityPub.Utils use Pleroma.DataCase + import Pleroma.Factory + setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -46,7 +48,8 @@ test "relay is followed" do describe "running unfollow" do test "relay is unfollowed" do - target_instance = "http://mastodon.example.org/users/admin" + user = insert(:user) + target_instance = user.ap_id Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) @@ -71,7 +74,7 @@ test "relay is unfollowed" do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"] == cancelled_activity.data + assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] refute "#{target_instance}/followers" in User.following(local_user) end end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index 9220d23fc..ce43a9cc7 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -110,7 +110,23 @@ test "user is deleted" do test "a remote user's create activity is deleted when the object has been pruned" do user = insert(:user) + user2 = insert(:user) + {:ok, post} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, post2} = CommonAPI.post(user2, %{status: "test"}) + obj = Object.normalize(post2) + + {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj) + + {:ok, like_activity, _meta} = + Pleroma.Web.ActivityPub.Pipeline.common_pipeline( + like_object, + Keyword.put(meta, :local, true) + ) + + like_activity.data["object"] + |> Pleroma.Object.get_by_ap_id() + |> Repo.delete() clear_config([:instance, :federating], true) @@ -127,6 +143,7 @@ test "a remote user's create activity is deleted when the object has been pruned assert %{deactivated: true} = User.get_by_nickname(user.nickname) assert called(Pleroma.Web.Federator.publish(:_)) + refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id) end refute Activity.get_by_id(post.id) @@ -464,17 +481,17 @@ test "it returns users matching" do moot = insert(:user, nickname: "moot") kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon") - {:ok, user} = User.follow(user, kawen) + {:ok, user} = User.follow(user, moon) assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id) - res = User.search("moo") |> Enum.map(& &1.id) - assert moon.id in res - assert moot.id in res - assert kawen.id in res - assert [moon.id, kawen.id] == User.Search.search("moon fediverse") |> Enum.map(& &1.id) - assert [kawen.id, moon.id] == - User.Search.search("moon fediverse", for_user: user) |> Enum.map(& &1.id) + res = User.search("moo") |> Enum.map(& &1.id) + assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res) + + assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id) + + assert [moon.id, kawen.id] == + User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id) end end diff --git a/test/upload/filter/exiftool_test.exs b/test/upload/filter/exiftool_test.exs new file mode 100644 index 000000000..a1b7e46cd --- /dev/null +++ b/test/upload/filter/exiftool_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.ExiftoolTest do + use Pleroma.DataCase + alias Pleroma.Upload.Filter + + test "apply exiftool filter" do + File.cp!( + "test/fixtures/DSCN0010.jpg", + "test/fixtures/DSCN0010_tmp.jpg" + ) + + upload = %Pleroma.Upload{ + name: "image_with_GPS_data.jpg", + content_type: "image/jpg", + path: Path.absname("test/fixtures/DSCN0010.jpg"), + tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg") + } + + assert Filter.Exiftool.filter(upload) == :ok + + {exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"]) + {exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"]) + + refute exif_original == exif_filtered + assert String.match?(exif_original, ~r/GPS/) + refute String.match?(exif_filtered, ~r/GPS/) + end +end diff --git a/test/upload_test.exs b/test/upload_test.exs index 2abf0edec..b06b54487 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -107,6 +107,19 @@ test "it returns error" do describe "Storing a file with the Local uploader" do setup [:ensure_local_uploader] + test "does not allow descriptions longer than the post limit" do + clear_config([:instance, :description_limit], 2) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:error, :description_too_long} = Upload.store(file, description: "123") + end + test "returns a media url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") diff --git a/test/user/notification_setting_test.exs b/test/user/notification_setting_test.exs index 95bca22c4..308da216a 100644 --- a/test/user/notification_setting_test.exs +++ b/test/user/notification_setting_test.exs @@ -8,11 +8,11 @@ defmodule Pleroma.User.NotificationSettingTest do alias Pleroma.User.NotificationSetting describe "changeset/2" do - test "sets valid privacy option" do + test "sets option to hide notification contents" do changeset = NotificationSetting.changeset( %NotificationSetting{}, - %{"privacy_option" => true} + %{"hide_notification_contents" => true} ) assert %Ecto.Changeset{valid?: true} = changeset diff --git a/test/user_search_test.exs b/test/user_search_test.exs index 17c63322a..559ba5966 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -17,7 +17,7 @@ defmodule Pleroma.UserSearchTest do describe "User.search" do setup do: clear_config([:instance, :limit_to_local_content]) - test "excluded invisible users from results" do + test "excludes invisible users from results" do user = insert(:user, %{nickname: "john t1000"}) insert(:user, %{invisible: true, nickname: "john t800"}) @@ -25,6 +25,15 @@ test "excluded invisible users from results" do assert found_user.id == user.id end + test "excludes service actors from results" do + insert(:user, actor_type: "Application", nickname: "user1") + service = insert(:user, actor_type: "Service", nickname: "user2") + person = insert(:user, actor_type: "Person", nickname: "user3") + + assert [found_user1, found_user2] = User.search("user") + assert [found_user1.id, found_user2.id] -- [service.id, person.id] == [] + end + test "accepts limit parameter" do Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) assert length(User.search("john", limit: 3)) == 3 @@ -37,30 +46,49 @@ test "accepts offset parameter" do assert length(User.search("john", limit: 3, offset: 3)) == 2 end - test "finds a user by full or partial nickname" do + defp clear_virtual_fields(user) do + Map.merge(user, %{search_rank: nil, search_type: nil}) + end + + test "finds a user by full nickname or its leading fragment" do user = insert(:user, %{nickname: "john"}) Enum.each(["john", "jo", "j"], fn query -> assert user == User.search(query) |> List.first() - |> Map.put(:search_rank, nil) - |> Map.put(:search_type, nil) + |> clear_virtual_fields() end) end - test "finds a user by full or partial name" do + test "finds a user by full name or leading fragment(s) of its words" do user = insert(:user, %{name: "John Doe"}) Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> assert user == User.search(query) |> List.first() - |> Map.put(:search_rank, nil) - |> Map.put(:search_type, nil) + |> clear_virtual_fields() end) end + test "matches by leading fragment of user domain" do + user = insert(:user, %{nickname: "arandom@dude.com"}) + insert(:user, %{nickname: "iamthedude"}) + + assert [user.id] == User.search("dud") |> Enum.map(& &1.id) + end + + test "ranks full nickname match higher than full name match" do + nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"}) + named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"}) + + results = User.search("hj") + + assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id) + assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank + end + test "finds users, considering density of matched tokens" do u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) diff --git a/test/user_test.exs b/test/user_test.exs index 9b66f3f51..9788e09d9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -486,6 +486,15 @@ test "it sets the password_hash and ap_id" do } setup do: clear_config([:instance, :account_activation_required], true) + test "it sets the 'accepts_chat_messages' set to true" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.accepts_chat_messages + end + test "it creates unconfirmed user" do changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? @@ -597,6 +606,31 @@ test "updates an existing user, if stale" do refute user.last_refreshed_at == orig_user.last_refreshed_at end + test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do + a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) + + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari", + last_refreshed_at: a_week_ago + ) + + assert orig_user.last_refreshed_at == a_week_ago + + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + + assert user.inbox + + refute user.id == orig_user.id + + orig_user = User.get_by_id(orig_user.id) + + assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + @tag capture_log: true test "it returns the old user if stale, but unfetchable" do a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index e722f7c04..ed900d8f8 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -1082,6 +1082,45 @@ test "it increases like count when receiving a like action", %{conn: conn} do 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"]) + assert cirno_object.data["actor"] == cirno.ap_id + assert cirno_object.data["attributedTo"] == cirno.ap_id + end end describe "/relay/followers" do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index be7ab2ae4..f3951462f 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -184,38 +184,45 @@ test "it returns a user that is invisible" do assert User.invisible?(user) end - test "it fetches the appropriate tag-restricted posts" do - user = insert(:user) + test "it returns a user that accepts chat messages" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) - {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) - {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) - {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - - fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - - fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) - - fetch_three = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test", "essais"], - tag_reject: ["reject"] - }) - - fetch_four = - ActivityPub.fetch_activities([], %{ - type: "Create", - tag: ["test"], - tag_all: ["test", "reject"] - }) - - assert fetch_one == [status_one, status_three] - assert fetch_two == [status_one, status_two, status_three] - assert fetch_three == [status_one, status_two] - assert fetch_four == [status_three] + assert user.accepts_chat_messages end end + test "it fetches the appropriate tag-restricted posts" do + user = insert(:user) + + {:ok, status_one} = CommonAPI.post(user, %{status: ". #test"}) + {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) + {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) + + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) + + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) + + fetch_three = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] + }) + + fetch_four = + ActivityPub.fetch_activities([], %{ + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] + }) + + assert fetch_one == [status_one, status_three] + assert fetch_two == [status_one, status_two, status_three] + assert fetch_three == [status_one, status_two] + assert fetch_four == [status_three] + end + describe "insertion" do test "drops activities beyond a certain limit" do limit = Config.get([:instance, :remote_limit]) @@ -507,6 +514,33 @@ test "retrieves activities that have a given context" do activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) assert activities == [activity_two, activity] end + + test "doesn't return activities with filtered words" do + user = insert(:user) + user_two = insert(:user) + insert(:filter, user: user, phrase: "test", hide: true) + + {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"}) + + {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1}) + + {:ok, %{id: id3} = user_activity} = + CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2}) + + {:ok, %{id: id4} = filtered_activity} = + CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3}) + + {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4}) + + activities = + context + |> ActivityPub.fetch_activities_for_context(%{user: user}) + |> Enum.map(& &1.id) + + assert length(activities) == 4 + assert user_activity.id in activities + refute filtered_activity.id in activities + end end test "doesn't return blocked activities" do @@ -642,7 +676,7 @@ test "doesn't return activities from blocked domains" do refute activity in activities followed_user = insert(:user) - ActivityPub.follow(user, followed_user) + CommonAPI.follow(user, followed_user) {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) @@ -785,6 +819,75 @@ test "excludes reblogs on request" do assert activity == expected_activity end + describe "irreversible filters" do + setup do + user = insert(:user) + user_two = insert(:user) + + insert(:filter, user: user_two, phrase: "cofe", hide: true) + insert(:filter, user: user_two, phrase: "ok boomer", hide: true) + insert(:filter, user: user_two, phrase: "test", hide: false) + + params = %{ + type: ["Create", "Announce"], + user: user_two + } + + {:ok, %{user: user, user_two: user_two, params: params}} + end + + test "it returns statuses if they don't contain exact filter words", %{ + user: user, + params: params + } do + {:ok, _} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"}) + {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"}) + {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"}) + {:ok, _} = CommonAPI.post(user, %{status: "this is a test"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 6 + end + + test "it does not filter user's own statuses", %{user_two: user_two, params: params} do + {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.count(activities) == 2 + end + + test "it excludes statuses with filter words", %{user: user, params: params} do + {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"}) + {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"}) + {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"}) + {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"}) + {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"}) + + activities = ActivityPub.fetch_activities([], params) + + assert Enum.empty?(activities) + end + + test "it returns all statuses if user does not have any filters" do + another_user = insert(:user) + {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"}) + {:ok, _} = CommonAPI.post(another_user, %{status: "test!"}) + + activities = + ActivityPub.fetch_activities([], %{ + type: ["Create", "Announce"], + user: another_user + }) + + assert Enum.count(activities) == 2 + end + end + describe "public fetch activities" do test "doesn't retrieve unlisted activities" do user = insert(:user) @@ -917,24 +1020,12 @@ test "fetches the latest Follow activity" do end end - describe "following / unfollowing" do - test "it reverts follow activity" do - follower = insert(:user) - followed = insert(:user) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.follow(follower, followed) - end - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - + describe "unfollowing" do test "it reverts unfollow activity" do follower = insert(:user) followed = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do assert {:error, :reverted} = ActivityPub.unfollow(follower, followed) @@ -947,21 +1038,11 @@ test "it reverts unfollow activity" do assert activity.data["object"] == followed.ap_id end - test "creates a follow activity" do - follower = insert(:user) - followed = insert(:user) - - {:ok, activity} = ActivityPub.follow(follower, followed) - assert activity.data["type"] == "Follow" - assert activity.data["actor"] == follower.ap_id - assert activity.data["object"] == followed.ap_id - end - test "creates an undo activity for the last follow" do follower = insert(:user) followed = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) {:ok, activity} = ActivityPub.unfollow(follower, followed) assert activity.data["type"] == "Undo" @@ -978,7 +1059,7 @@ test "creates an undo activity for a pending follow request" do follower = insert(:user) followed = insert(:user, %{locked: true}) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) {:ok, activity} = ActivityPub.unfollow(follower, followed) assert activity.data["type"] == "Undo" @@ -992,54 +1073,6 @@ test "creates an undo activity for a pending follow request" do end end - describe "blocking" do - test "reverts block activity on error" do - [blocker, blocked] = insert_list(2, :user) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.block(blocker, blocked) - end - - assert Repo.aggregate(Activity, :count, :id) == 0 - assert Repo.aggregate(Object, :count, :id) == 0 - end - - test "creates a block activity" do - clear_config([:instance, :federating], true) - blocker = insert(:user) - blocked = insert(:user) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - {:ok, activity} = ActivityPub.block(blocker, blocked) - - assert activity.data["type"] == "Block" - assert activity.data["actor"] == blocker.ap_id - assert activity.data["object"] == blocked.ap_id - - assert called(Pleroma.Web.Federator.publish(activity)) - end - end - - test "works with outgoing blocks disabled, but doesn't federate" do - clear_config([:instance, :federating], true) - clear_config([:activitypub, :outgoing_blocks], false) - blocker = insert(:user) - blocked = insert(:user) - - with_mock Pleroma.Web.Federator, - publish: fn _ -> nil end do - {:ok, activity} = ActivityPub.block(blocker, blocked) - - assert activity.data["type"] == "Block" - assert activity.data["actor"] == blocker.ap_id - assert activity.data["object"] == blocked.ap_id - - refute called(Pleroma.Web.Federator.publish(:_)) - end - end - end - describe "timeline post-processing" do test "it filters broken threads" do user1 = insert(:user) @@ -1411,7 +1444,7 @@ test "create" do assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) - Pleroma.Workers.BackgroundWorker.perform(params, nil) + Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) refute User.following?(follower, old_user) assert User.following?(follower, new_user) @@ -2023,4 +2056,46 @@ test "creates an activity expiration for local Create activities" do assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all() end end + + describe "handling of clashing nicknames" do + test "renames an existing user with a clashing nickname and a different ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id <> "part_2" + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == "#{orig_user.id}.admin@mastodon.example.org" + end + + test "does nothing with a clashing nickname and the same ap id" do + orig_user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: "http://mastodon.example.org/users/harinezumigari" + ) + + %{ + nickname: orig_user.nickname, + ap_id: orig_user.ap_id + } + |> ActivityPub.maybe_handle_clashing_nickname() + + user = User.get_by_id(orig_user.id) + + assert user.nickname == orig_user.nickname + end + end end diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs index fca0de7c6..3c795f5ac 100644 --- a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -21,7 +21,7 @@ test "matches followbots by nickname" do "id" => "https://example.com/activities/1234" } - {:reject, nil} = AntiFollowbotPolicy.filter(message) + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) end test "matches followbots by display name" do @@ -36,7 +36,7 @@ test "matches followbots by display name" do "id" => "https://example.com/activities/1234" } - {:reject, nil} = AntiFollowbotPolicy.filter(message) + assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message) end end diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs index 1a13699be..6867c9853 100644 --- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -33,7 +33,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do describe "with new user" do test "it allows posts without links" do - user = insert(:user) + user = insert(:user, local: false) assert user.note_count == 0 @@ -45,7 +45,7 @@ test "it allows posts without links" do end test "it disallows posts with links" do - user = insert(:user) + user = insert(:user, local: false) assert user.note_count == 0 @@ -55,6 +55,18 @@ test "it disallows posts with links" do {:reject, _} = AntiLinkSpamPolicy.filter(message) end + + test "it allows posts with links for local users" do + user = insert(:user) + + assert user.note_count == 0 + + message = + @linkful_message + |> Map.put("actor", user.ap_id) + + {:ok, _message} = AntiLinkSpamPolicy.filter(message) + end end describe "with old user" do diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs index 6e9daa7f9..26f5bcdaa 100644 --- a/test/web/activity_pub/mrf/hellthread_policy_test.exs +++ b/test/web/activity_pub/mrf/hellthread_policy_test.exs @@ -50,7 +50,8 @@ test "rejects the message if the recipient count is above reject_threshold", %{ } do Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) - {:reject, nil} = filter(message) + assert {:reject, "[HellthreadPolicy] 3 recipients is over the limit of 2"} == + filter(message) end test "does not reject the message if the recipient count is below reject_threshold", %{ diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs index fd1f7aec8..b3d0f3d90 100644 --- a/test/web/activity_pub/mrf/keyword_policy_test.exs +++ b/test/web/activity_pub/mrf/keyword_policy_test.exs @@ -25,7 +25,8 @@ test "rejects if string matches in content" do } } - assert {:reject, nil} == KeywordPolicy.filter(message) + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) end test "rejects if string matches in summary" do @@ -39,7 +40,8 @@ test "rejects if string matches in summary" do } } - assert {:reject, nil} == KeywordPolicy.filter(message) + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) end test "rejects if regex matches in content" do @@ -55,7 +57,8 @@ test "rejects if regex matches in content" do } } - {:reject, nil} == KeywordPolicy.filter(message) + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) end) end @@ -72,7 +75,8 @@ test "rejects if regex matches in summary" do } } - {:reject, nil} == KeywordPolicy.filter(message) + {:reject, "[KeywordPolicy] Matches with rejected keyword"} == + KeywordPolicy.filter(message) end) end end diff --git a/test/web/activity_pub/mrf/mention_policy_test.exs b/test/web/activity_pub/mrf/mention_policy_test.exs index aa003bef5..220309cc9 100644 --- a/test/web/activity_pub/mrf/mention_policy_test.exs +++ b/test/web/activity_pub/mrf/mention_policy_test.exs @@ -76,7 +76,8 @@ test "to" do "to" => ["https://example.com/blocked"] } - assert MentionPolicy.filter(message) == {:reject, nil} + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} end test "cc" do @@ -88,7 +89,8 @@ test "cc" do "cc" => ["https://example.com/blocked"] } - assert MentionPolicy.filter(message) == {:reject, nil} + assert MentionPolicy.filter(message) == + {:reject, "[MentionPolicy] Rejected for mention of https://example.com/blocked"} end end end diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs index f36299b86..58b46b9a2 100644 --- a/test/web/activity_pub/mrf/reject_non_public_test.exs +++ b/test/web/activity_pub/mrf/reject_non_public_test.exs @@ -64,7 +64,7 @@ test "it's rejected when addrer of message in the follower addresses of user and } Pleroma.Config.put([:mrf_rejectnonpublic, :allow_followersonly], false) - assert {:reject, nil} = RejectNonPublic.filter(message) + assert {:reject, _} = RejectNonPublic.filter(message) end end @@ -94,7 +94,7 @@ test "it's reject when direct messages aren't allow" do } Pleroma.Config.put([:mrf_rejectnonpublic, :allow_direct], false) - assert {:reject, nil} = RejectNonPublic.filter(message) + assert {:reject, _} = RejectNonPublic.filter(message) end end end diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index b7b9bc6a2..e842d8d8d 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -124,7 +124,7 @@ test "has a matching host" do report_message = build_report_message() local_message = build_local_message() - assert SimplePolicy.filter(report_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(report_message) assert SimplePolicy.filter(local_message) == {:ok, local_message} end @@ -133,7 +133,7 @@ test "match with wildcard domain" do report_message = build_report_message() local_message = build_local_message() - assert SimplePolicy.filter(report_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(report_message) assert SimplePolicy.filter(local_message) == {:ok, local_message} end end @@ -241,7 +241,7 @@ test "activity has a matching host" do remote_message = build_remote_message() - assert SimplePolicy.filter(remote_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(remote_message) end test "activity matches with wildcard domain" do @@ -249,7 +249,7 @@ test "activity matches with wildcard domain" do remote_message = build_remote_message() - assert SimplePolicy.filter(remote_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(remote_message) end test "actor has a matching host" do @@ -257,7 +257,7 @@ test "actor has a matching host" do remote_user = build_remote_user() - assert SimplePolicy.filter(remote_user) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(remote_user) end end @@ -279,7 +279,7 @@ test "is not empty but activity doesn't have a matching host" do remote_message = build_remote_message() assert SimplePolicy.filter(local_message) == {:ok, local_message} - assert SimplePolicy.filter(remote_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(remote_message) end test "activity has a matching host" do @@ -429,7 +429,7 @@ test "it accepts deletions even from non-whitelisted servers" do test "it rejects the deletion" do deletion_message = build_remote_deletion_message() - assert SimplePolicy.filter(deletion_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(deletion_message) end end @@ -439,7 +439,7 @@ test "it rejects the deletion" do test "it rejects the deletion" do deletion_message = build_remote_deletion_message() - assert SimplePolicy.filter(deletion_message) == {:reject, nil} + assert {:reject, _} = SimplePolicy.filter(deletion_message) end end diff --git a/test/web/activity_pub/mrf/tag_policy_test.exs b/test/web/activity_pub/mrf/tag_policy_test.exs index e7793641a..6ff71d640 100644 --- a/test/web/activity_pub/mrf/tag_policy_test.exs +++ b/test/web/activity_pub/mrf/tag_policy_test.exs @@ -12,8 +12,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do describe "mrf_tag:disable-any-subscription" do test "rejects message" do actor = insert(:user, tags: ["mrf_tag:disable-any-subscription"]) - message = %{"object" => actor.ap_id, "type" => "Follow"} - assert {:reject, nil} = TagPolicy.filter(message) + message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => actor.ap_id} + assert {:reject, _} = TagPolicy.filter(message) end end @@ -22,7 +22,7 @@ test "rejects non-local follow requests" do actor = insert(:user, tags: ["mrf_tag:disable-remote-subscription"]) follower = insert(:user, tags: ["mrf_tag:disable-remote-subscription"], local: false) message = %{"object" => actor.ap_id, "type" => "Follow", "actor" => follower.ap_id} - assert {:reject, nil} = TagPolicy.filter(message) + assert {:reject, _} = TagPolicy.filter(message) end test "allows non-local follow requests" do diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs index ba1b69658..8e1ad5bc8 100644 --- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs +++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs @@ -26,6 +26,6 @@ test "rejected if allow list isn't empty and user not in allow list" do actor = insert(:user) Pleroma.Config.put([:mrf_user_allowlist], %{"localhost" => ["test-ap-id"]}) message = %{"actor" => actor.ap_id} - assert UserAllowListPolicy.filter(message) == {:reject, nil} + assert {:reject, _} = UserAllowListPolicy.filter(message) end end diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs index 69f22bb77..2bceb67ee 100644 --- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs +++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -46,7 +46,7 @@ test "it does not accept disallowed child objects" do } } - {:reject, nil} = VocabularyPolicy.filter(message) + {:reject, _} = VocabularyPolicy.filter(message) end test "it does not accept disallowed parent types" do @@ -60,7 +60,7 @@ test "it does not accept disallowed parent types" do } } - {:reject, nil} = VocabularyPolicy.filter(message) + {:reject, _} = VocabularyPolicy.filter(message) end end @@ -75,7 +75,7 @@ test "it rejects based on parent activity type" do "object" => "whatever" } - {:reject, nil} = VocabularyPolicy.filter(message) + {:reject, _} = VocabularyPolicy.filter(message) end test "it rejects based on child object type" do @@ -89,7 +89,7 @@ test "it rejects based on child object type" do } } - {:reject, nil} = VocabularyPolicy.filter(message) + {:reject, _} = VocabularyPolicy.filter(message) end test "it passes through objects that aren't disallowed" do diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs deleted file mode 100644 index 770a8dcf8..000000000 --- a/test/web/activity_pub/object_validator_test.exs +++ /dev/null @@ -1,657 +0,0 @@ -defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do - use Pleroma.DataCase - - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder - alias Pleroma.Web.ActivityPub.ObjectValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - - describe "attachments" do - test "works with honkerific attachments" do - attachment = %{ - "mediaType" => "", - "name" => "", - "summary" => "298p3RG7j27tfsZ9RQ.jpg", - "type" => "Document", - "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" - } - - assert {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "application/octet-stream" - end - - test "it turns mastodon attachments into our attachments" do - attachment = %{ - "url" => - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - "type" => "Document", - "name" => nil, - "mediaType" => "image/jpeg" - } - - {:ok, attachment} = - AttachmentValidator.cast_and_validate(attachment) - |> Ecto.Changeset.apply_action(:insert) - - assert [ - %{ - href: - "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", - type: "Link", - mediaType: "image/jpeg" - } - ] = attachment.url - - assert attachment.mediaType == "image/jpeg" - end - - test "it handles our own uploads" do - user = insert(:user) - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - {:ok, attachment} = - attachment.data - |> AttachmentValidator.cast_and_validate() - |> Ecto.Changeset.apply_action(:insert) - - assert attachment.mediaType == "image/jpeg" - end - end - - describe "chat message create activities" do - test "it is invalid if the object already exists" do - user = insert(:user) - recipient = insert(:user) - {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") - object = Object.normalize(activity, false) - - {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:object, {"The object to create already exists", []}} in cng.errors - end - - test "it is invalid if the object data has a different `to` or `actor` field" do - user = insert(:user) - recipient = insert(:user) - {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") - - {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) - - {:error, cng} = ObjectValidator.validate(create_data, []) - - assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors - assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors - end - end - - describe "chat messages" do - setup do - clear_config([:instance, :remote_limit]) - user = insert(:user) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") - - %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} - end - - test "let's through some basic html", %{user: user, recipient: recipient} do - {:ok, valid_chat_message, _} = - Builder.chat_message( - user, - recipient.ap_id, - "hey example " - ) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["content"] == - "hey example alert('uguu')" - end - - test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert Map.put(valid_chat_message, "attachment", nil) == object - end - - test "validates for a basic object with an attachment", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment in an array", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", [attachment.data]) - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "validates for a basic object with an attachment but without content", %{ - valid_chat_message: valid_chat_message, - user: user - } do - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image.jpg"), - filename: "an_image.jpg" - } - - {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) - - valid_chat_message = - valid_chat_message - |> Map.put("attachment", attachment.data) - |> Map.delete("content") - - assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) - - assert object["attachment"] - end - - test "does not validate if the message has no content", %{ - valid_chat_message: valid_chat_message - } do - contentless = - valid_chat_message - |> Map.delete("content") - - refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) - end - - test "does not validate if the message is longer than the remote_limit", %{ - valid_chat_message: valid_chat_message - } do - Pleroma.Config.put([:instance, :remote_limit], 2) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the recipient is blocking the actor", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - Pleroma.User.block(recipient, user) - refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) - end - - test "does not validate if the actor or the recipient is not in our system", %{ - valid_chat_message: valid_chat_message - } do - chat_message = - valid_chat_message - |> Map.put("actor", "https://raymoo.com/raymoo") - - {:error, _} = ObjectValidator.validate(chat_message, []) - - chat_message = - valid_chat_message - |> Map.put("to", ["https://raymoo.com/raymoo"]) - - {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate for a message with multiple recipients", %{ - valid_chat_message: valid_chat_message, - user: user, - recipient: recipient - } do - chat_message = - valid_chat_message - |> Map.put("to", [user.ap_id, recipient.ap_id]) - - assert {:error, _} = ObjectValidator.validate(chat_message, []) - end - - test "does not validate if it doesn't concern local users" do - user = insert(:user, local: false) - recipient = insert(:user, local: false) - - {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") - assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) - end - end - - describe "EmojiReacts" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) - - {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") - - %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} - end - - test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do - assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) - end - - test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do - without_content = - valid_emoji_react - |> Map.delete("content") - - {:error, cng} = ObjectValidator.validate(without_content, []) - - refute cng.valid? - assert {:content, {"can't be blank", [validation: :required]}} in cng.errors - end - - test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do - without_emoji_content = - valid_emoji_react - |> Map.put("content", "x") - - {:error, cng} = ObjectValidator.validate(without_emoji_content, []) - - refute cng.valid? - - assert {:content, {"must be a single character emoji", []}} in cng.errors - end - end - - describe "Undos" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - {:ok, like} = CommonAPI.favorite(user, post_activity.id) - {:ok, valid_like_undo, []} = Builder.undo(user, like) - - %{user: user, like: like, valid_like_undo: valid_like_undo} - end - - test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do - assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) - end - - test "it does not validate if the actor of the undo is not the actor of the object", %{ - valid_like_undo: valid_like_undo - } do - other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") - - bad_actor = - valid_like_undo - |> Map.put("actor", other_user.ap_id) - - {:error, cng} = ObjectValidator.validate(bad_actor, []) - - assert {:actor, {"not the same as object actor", []}} in cng.errors - end - - test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do - missing_object = - valid_like_undo - |> Map.put("object", "https://gensokyo.2hu/objects/1") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - assert length(cng.errors) == 1 - end - end - - describe "deletes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) - - {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) - {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) - - %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} - end - - test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do - {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) - - assert valid_post_delete["deleted_activity_id"] - end - - test "it is invalid if the object isn't in a list of certain types", %{ - valid_post_delete: valid_post_delete - } do - object = Object.get_by_ap_id(valid_post_delete["object"]) - - data = - object.data - |> Map.put("type", "Like") - - {:ok, _object} = - object - |> Ecto.Changeset.change(%{data: data}) - |> Object.update_and_set_cache() - - {:error, cng} = ObjectValidator.validate(valid_post_delete, []) - assert {:object, {"object not in allowed types", []}} in cng.errors - end - - test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do - assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) - end - - test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do - no_id = - valid_post_delete - |> Map.delete("id") - - {:error, cng} = ObjectValidator.validate(no_id, []) - - assert {:id, {"can't be blank", [validation: :required]}} in cng.errors - end - - test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do - missing_object = - valid_post_delete - |> Map.put("object", "http://does.not/exist") - - {:error, cng} = ObjectValidator.validate(missing_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "it's invalid if the actor of the object and the actor of delete are from different domains", - %{valid_post_delete: valid_post_delete} do - valid_user = insert(:user) - - valid_other_actor = - valid_post_delete - |> Map.put("actor", valid_user.ap_id) - - assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) - - invalid_other_actor = - valid_post_delete - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) - - assert {:actor, {"is not allowed to delete object", []}} in cng.errors - end - - test "it's valid if the actor of the object is a local superuser", - %{valid_post_delete: valid_post_delete} do - user = - insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") - - valid_other_actor = - valid_post_delete - |> Map.put("actor", user.ap_id) - - {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) - assert meta[:do_not_federate] - end - end - - describe "likes" do - setup do - user = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - valid_like = %{ - "to" => [user.ap_id], - "cc" => [], - "type" => "Like", - "id" => Utils.generate_activity_id(), - "object" => post_activity.data["object"], - "actor" => user.ap_id, - "context" => "a context" - } - - %{valid_like: valid_like, user: user, post_activity: post_activity} - end - - test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do - {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) - - assert "id" in Map.keys(object) - end - - test "is valid for a valid object", %{valid_like: valid_like} do - assert LikeValidator.cast_and_validate(valid_like).valid? - end - - test "sets the 'to' field to the object actor if no recipients are given", %{ - valid_like: valid_like, - user: user - } do - without_recipients = - valid_like - |> Map.delete("to") - - {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) - - assert object["to"] == [user.ap_id] - end - - test "sets the context field to the context of the object if no context is given", %{ - valid_like: valid_like, - post_activity: post_activity - } do - without_context = - valid_like - |> Map.delete("context") - - {:ok, object, _meta} = ObjectValidator.validate(without_context, []) - - assert object["context"] == post_activity.data["context"] - end - - test "it errors when the actor is missing or not known", %{valid_like: valid_like} do - without_actor = Map.delete(valid_like, "actor") - - refute LikeValidator.cast_and_validate(without_actor).valid? - - with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") - - refute LikeValidator.cast_and_validate(with_invalid_actor).valid? - end - - test "it errors when the object is missing or not known", %{valid_like: valid_like} do - without_object = Map.delete(valid_like, "object") - - refute LikeValidator.cast_and_validate(without_object).valid? - - with_invalid_object = Map.put(valid_like, "object", "invalidobject") - - refute LikeValidator.cast_and_validate(with_invalid_object).valid? - end - - test "it errors when the actor has already like the object", %{ - valid_like: valid_like, - user: user, - post_activity: post_activity - } do - _like = CommonAPI.favorite(user, post_activity.id) - - refute LikeValidator.cast_and_validate(valid_like).valid? - end - - test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do - wrapped_like = - valid_like - |> Map.put("actor", %{"id" => valid_like["actor"]}) - |> Map.put("object", %{"id" => valid_like["object"]}) - - validated = LikeValidator.cast_and_validate(wrapped_like) - - assert validated.valid? - - assert {:actor, valid_like["actor"]} in validated.changes - assert {:object, valid_like["object"]} in validated.changes - end - end - - describe "announces" do - setup do - user = insert(:user) - announcer = insert(:user) - {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) - - object = Object.normalize(post_activity, false) - {:ok, valid_announce, []} = Builder.announce(announcer, object) - - %{ - valid_announce: valid_announce, - user: user, - post_activity: post_activity, - announcer: announcer - } - end - - test "returns ok for a valid announce", %{valid_announce: valid_announce} do - assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) - end - - test "returns an error if the object can't be found", %{valid_announce: valid_announce} do - without_object = - valid_announce - |> Map.delete("object") - - {:error, cng} = ObjectValidator.validate(without_object, []) - - assert {:object, {"can't be blank", [validation: :required]}} in cng.errors - - nonexisting_object = - valid_announce - |> Map.put("object", "https://gensokyo.2hu/objects/99999999") - - {:error, cng} = ObjectValidator.validate(nonexisting_object, []) - - assert {:object, {"can't find object", []}} in cng.errors - end - - test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do - nonexisting_actor = - valid_announce - |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") - - {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) - - assert {:actor, {"can't find user", []}} in cng.errors - end - - test "returns an error if the actor already announced the object", %{ - valid_announce: valid_announce, - announcer: announcer, - post_activity: post_activity - } do - _announce = CommonAPI.repeat(post_activity.id, announcer) - - {:error, cng} = ObjectValidator.validate(valid_announce, []) - - assert {:actor, {"already announced this object", []}} in cng.errors - assert {:object, {"already announced by this actor", []}} in cng.errors - end - - test "returns an error if the actor can't announce the object", %{ - announcer: announcer, - user: user - } do - {:ok, post_activity} = - CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) - - object = Object.normalize(post_activity, false) - - # Another user can't announce it - {:ok, announce, []} = Builder.announce(announcer, object, public: false) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object", []}} in cng.errors - - # The actor of the object can announce it - {:ok, announce, []} = Builder.announce(user, object, public: false) - - assert {:ok, _, _} = ObjectValidator.validate(announce, []) - - # The actor of the object can not announce it publicly - {:ok, announce, []} = Builder.announce(user, object, public: true) - - {:error, cng} = ObjectValidator.validate(announce, []) - - assert {:actor, {"can not announce this object publicly", []}} in cng.errors - end - end - - describe "updates" do - setup do - user = insert(:user) - - object = %{ - "id" => user.ap_id, - "name" => "A new name", - "summary" => "A new bio" - } - - {:ok, valid_update, []} = Builder.update(user, object) - - %{user: user, valid_update: valid_update} - end - - test "validates a basic object", %{valid_update: valid_update} do - assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) - end - - test "returns an error if the object can't be updated by the actor", %{ - valid_update: valid_update - } do - other_user = insert(:user) - - update = - valid_update - |> Map.put("actor", other_user.ap_id) - - assert {:error, _cng} = ObjectValidator.validate(update, []) - end - end -end diff --git a/test/web/activity_pub/object_validators/announce_validation_test.exs b/test/web/activity_pub/object_validators/announce_validation_test.exs new file mode 100644 index 000000000..623342f76 --- /dev/null +++ b/test/web/activity_pub/object_validators/announce_validation_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnouncValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "announces" do + setup do + user = insert(:user) + announcer = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Object.normalize(post_activity, false) + {:ok, valid_announce, []} = Builder.announce(announcer, object) + + %{ + valid_announce: valid_announce, + user: user, + post_activity: post_activity, + announcer: announcer + } + end + + test "returns ok for a valid announce", %{valid_announce: valid_announce} do + assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, []) + end + + test "returns an error if the object can't be found", %{valid_announce: valid_announce} do + without_object = + valid_announce + |> Map.delete("object") + + {:error, cng} = ObjectValidator.validate(without_object, []) + + assert {:object, {"can't be blank", [validation: :required]}} in cng.errors + + nonexisting_object = + valid_announce + |> Map.put("object", "https://gensokyo.2hu/objects/99999999") + + {:error, cng} = ObjectValidator.validate(nonexisting_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do + nonexisting_actor = + valid_announce + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(nonexisting_actor, []) + + assert {:actor, {"can't find user", []}} in cng.errors + end + + test "returns an error if the actor already announced the object", %{ + valid_announce: valid_announce, + announcer: announcer, + post_activity: post_activity + } do + _announce = CommonAPI.repeat(post_activity.id, announcer) + + {:error, cng} = ObjectValidator.validate(valid_announce, []) + + assert {:actor, {"already announced this object", []}} in cng.errors + assert {:object, {"already announced by this actor", []}} in cng.errors + end + + test "returns an error if the actor can't announce the object", %{ + announcer: announcer, + user: user + } do + {:ok, post_activity} = + CommonAPI.post(user, %{status: "a secret post", visibility: "private"}) + + object = Object.normalize(post_activity, false) + + # Another user can't announce it + {:ok, announce, []} = Builder.announce(announcer, object, public: false) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object", []}} in cng.errors + + # The actor of the object can announce it + {:ok, announce, []} = Builder.announce(user, object, public: false) + + assert {:ok, _, _} = ObjectValidator.validate(announce, []) + + # The actor of the object can not announce it publicly + {:ok, announce, []} = Builder.announce(user, object, public: true) + + {:error, cng} = ObjectValidator.validate(announce, []) + + assert {:actor, {"can not announce this object publicly", []}} in cng.errors + end + end +end diff --git a/test/web/activity_pub/object_validators/attachment_validator_test.exs b/test/web/activity_pub/object_validators/attachment_validator_test.exs new file mode 100644 index 000000000..558bb3131 --- /dev/null +++ b/test/web/activity_pub/object_validators/attachment_validator_test.exs @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + + import Pleroma.Factory + + describe "attachments" do + test "works with honkerific attachments" do + attachment = %{ + "mediaType" => "", + "name" => "", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "application/octet-stream" + end + + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + + assert attachment.mediaType == "image/jpeg" + end + + test "it handles our own uploads" do + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, attachment} = + attachment.data + |> AttachmentValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/jpeg" + end + end +end diff --git a/test/web/activity_pub/object_validators/block_validation_test.exs b/test/web/activity_pub/object_validators/block_validation_test.exs new file mode 100644 index 000000000..c08d4b2e8 --- /dev/null +++ b/test/web/activity_pub/object_validators/block_validation_test.exs @@ -0,0 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "blocks" do + setup do + user = insert(:user, local: false) + blocked = insert(:user) + + {:ok, valid_block, []} = Builder.block(user, blocked) + + %{user: user, valid_block: valid_block} + end + + test "validates a basic object", %{ + valid_block: valid_block + } do + assert {:ok, _block, []} = ObjectValidator.validate(valid_block, []) + end + + test "returns an error if we don't know the blocked user", %{ + valid_block: valid_block + } do + block = + valid_block + |> Map.put("object", "https://gensokyo.2hu/users/raymoo") + + assert {:error, _cng} = ObjectValidator.validate(block, []) + end + end +end diff --git a/test/web/activity_pub/object_validators/chat_validation_test.exs b/test/web/activity_pub/object_validators/chat_validation_test.exs new file mode 100644 index 000000000..50bf03515 --- /dev/null +++ b/test/web/activity_pub/object_validators/chat_validation_test.exs @@ -0,0 +1,211 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do + use Pleroma.DataCase + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end + end + + describe "chat messages" do + setup do + clear_config([:instance, :remote_limit]) + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "let's through some basic html", %{user: user, recipient: recipient} do + {:ok, valid_chat_message, _} = + Builder.chat_message( + user, + recipient.ap_id, + "hey example " + ) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["content"] == + "hey example alert('uguu')" + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert Map.put(valid_chat_message, "attachment", nil) == object + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment in an array", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", [attachment.data]) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment but without content", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + |> Map.delete("content") + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message has no content", %{ + valid_chat_message: valid_chat_message + } do + contentless = + valid_chat_message + |> Map.delete("content") + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) + end + + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is not accepting chat messages", %{ + valid_chat_message: valid_chat_message, + recipient: recipient + } do + recipient + |> Ecto.Changeset.change(%{accepts_chat_messages: false}) + |> Pleroma.Repo.update!() + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + end +end diff --git a/test/web/activity_pub/object_validators/delete_validation_test.exs b/test/web/activity_pub/object_validators/delete_validation_test.exs new file mode 100644 index 000000000..42cd18298 --- /dev/null +++ b/test/web/activity_pub/object_validators/delete_validation_test.exs @@ -0,0 +1,106 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do + use Pleroma.DataCase + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "deletes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"}) + + {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) + {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) + + %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} + end + + test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do + {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) + + assert valid_post_delete["deleted_activity_id"] + end + + test "it is invalid if the object isn't in a list of certain types", %{ + valid_post_delete: valid_post_delete + } do + object = Object.get_by_ap_id(valid_post_delete["object"]) + + data = + object.data + |> Map.put("type", "Like") + + {:ok, _object} = + object + |> Ecto.Changeset.change(%{data: data}) + |> Object.update_and_set_cache() + + {:error, cng} = ObjectValidator.validate(valid_post_delete, []) + assert {:object, {"object not in allowed types", []}} in cng.errors + end + + test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do + assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) + end + + test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do + no_id = + valid_post_delete + |> Map.delete("id") + + {:error, cng} = ObjectValidator.validate(no_id, []) + + assert {:id, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do + missing_object = + valid_post_delete + |> Map.put("object", "http://does.not/exist") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "it's invalid if the actor of the object and the actor of delete are from different domains", + %{valid_post_delete: valid_post_delete} do + valid_user = insert(:user) + + valid_other_actor = + valid_post_delete + |> Map.put("actor", valid_user.ap_id) + + assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) + + invalid_other_actor = + valid_post_delete + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) + + assert {:actor, {"is not allowed to delete object", []}} in cng.errors + end + + test "it's valid if the actor of the object is a local superuser", + %{valid_post_delete: valid_post_delete} do + user = + insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") + + valid_other_actor = + valid_post_delete + |> Map.put("actor", user.ap_id) + + {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + assert meta[:do_not_federate] + end + end +end diff --git a/test/web/activity_pub/object_validators/emoji_react_validation_test.exs b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs new file mode 100644 index 000000000..582e6d785 --- /dev/null +++ b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "EmojiReacts" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) + + {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") + + %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} + end + + test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do + assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) + end + + test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do + without_content = + valid_emoji_react + |> Map.delete("content") + + {:error, cng} = ObjectValidator.validate(without_content, []) + + refute cng.valid? + assert {:content, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do + without_emoji_content = + valid_emoji_react + |> Map.put("content", "x") + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:content, {"must be a single character emoji", []}} in cng.errors + end + end +end diff --git a/test/web/activity_pub/object_validators/follow_validation_test.exs b/test/web/activity_pub/object_validators/follow_validation_test.exs new file mode 100644 index 000000000..6e1378be2 --- /dev/null +++ b/test/web/activity_pub/object_validators/follow_validation_test.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "Follows" do + setup do + follower = insert(:user) + followed = insert(:user) + + {:ok, valid_follow, []} = Builder.follow(follower, followed) + %{follower: follower, followed: followed, valid_follow: valid_follow} + end + + test "validates a basic follow object", %{valid_follow: valid_follow} do + assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, []) + end + end +end diff --git a/test/web/activity_pub/object_validators/like_validation_test.exs b/test/web/activity_pub/object_validators/like_validation_test.exs new file mode 100644 index 000000000..2c033b7e2 --- /dev/null +++ b/test/web/activity_pub/object_validators/like_validation_test.exs @@ -0,0 +1,113 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "likes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + + valid_like = %{ + "to" => [user.ap_id], + "cc" => [], + "type" => "Like", + "id" => Utils.generate_activity_id(), + "object" => post_activity.data["object"], + "actor" => user.ap_id, + "context" => "a context" + } + + %{valid_like: valid_like, user: user, post_activity: post_activity} + end + + test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do + {:ok, object, _meta} = ObjectValidator.validate(valid_like, []) + + assert "id" in Map.keys(object) + end + + test "is valid for a valid object", %{valid_like: valid_like} do + assert LikeValidator.cast_and_validate(valid_like).valid? + end + + test "sets the 'to' field to the object actor if no recipients are given", %{ + valid_like: valid_like, + user: user + } do + without_recipients = + valid_like + |> Map.delete("to") + + {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) + + assert object["to"] == [user.ap_id] + end + + test "sets the context field to the context of the object if no context is given", %{ + valid_like: valid_like, + post_activity: post_activity + } do + without_context = + valid_like + |> Map.delete("context") + + {:ok, object, _meta} = ObjectValidator.validate(without_context, []) + + assert object["context"] == post_activity.data["context"] + end + + test "it errors when the actor is missing or not known", %{valid_like: valid_like} do + without_actor = Map.delete(valid_like, "actor") + + refute LikeValidator.cast_and_validate(without_actor).valid? + + with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") + + refute LikeValidator.cast_and_validate(with_invalid_actor).valid? + end + + test "it errors when the object is missing or not known", %{valid_like: valid_like} do + without_object = Map.delete(valid_like, "object") + + refute LikeValidator.cast_and_validate(without_object).valid? + + with_invalid_object = Map.put(valid_like, "object", "invalidobject") + + refute LikeValidator.cast_and_validate(with_invalid_object).valid? + end + + test "it errors when the actor has already like the object", %{ + valid_like: valid_like, + user: user, + post_activity: post_activity + } do + _like = CommonAPI.favorite(user, post_activity.id) + + refute LikeValidator.cast_and_validate(valid_like).valid? + end + + test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do + wrapped_like = + valid_like + |> Map.put("actor", %{"id" => valid_like["actor"]}) + |> Map.put("object", %{"id" => valid_like["object"]}) + + validated = LikeValidator.cast_and_validate(wrapped_like) + + assert validated.valid? + + assert {:actor, valid_like["actor"]} in validated.changes + assert {:object, valid_like["object"]} in validated.changes + end + end +end diff --git a/test/web/activity_pub/object_validators/undo_validation_test.exs b/test/web/activity_pub/object_validators/undo_validation_test.exs new file mode 100644 index 000000000..75bbcc4b6 --- /dev/null +++ b/test/web/activity_pub/object_validators/undo_validation_test.exs @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "Undos" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"}) + {:ok, like} = CommonAPI.favorite(user, post_activity.id) + {:ok, valid_like_undo, []} = Builder.undo(user, like) + + %{user: user, like: like, valid_like_undo: valid_like_undo} + end + + test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do + assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) + end + + test "it does not validate if the actor of the undo is not the actor of the object", %{ + valid_like_undo: valid_like_undo + } do + other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + bad_actor = + valid_like_undo + |> Map.put("actor", other_user.ap_id) + + {:error, cng} = ObjectValidator.validate(bad_actor, []) + + assert {:actor, {"not the same as object actor", []}} in cng.errors + end + + test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do + missing_object = + valid_like_undo + |> Map.put("object", "https://gensokyo.2hu/objects/1") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + assert length(cng.errors) == 1 + end + end +end diff --git a/test/web/activity_pub/object_validators/update_validation_test.exs b/test/web/activity_pub/object_validators/update_validation_test.exs new file mode 100644 index 000000000..5e80cf731 --- /dev/null +++ b/test/web/activity_pub/object_validators/update_validation_test.exs @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.ActivityPub.ObjectValidator + + import Pleroma.Factory + + describe "updates" do + setup do + user = insert(:user) + + object = %{ + "id" => user.ap_id, + "name" => "A new name", + "summary" => "A new bio" + } + + {:ok, valid_update, []} = Builder.update(user, object) + + %{user: user, valid_update: valid_update} + end + + test "validates a basic object", %{valid_update: valid_update} do + assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) + end + + test "returns an error if the object can't be updated by the actor", %{ + valid_update: valid_update + } do + other_user = insert(:user) + + update = + valid_update + |> Map.put("actor", other_user.ap_id) + + assert {:error, _cng} = ObjectValidator.validate(update, []) + end + end +end diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs index c2bc38d52..b9388b966 100644 --- a/test/web/activity_pub/publisher_test.exs +++ b/test/web/activity_pub/publisher_test.exs @@ -123,6 +123,39 @@ test "it returns inbox for messages involving single recipients in total" do end describe "publish_one/1" do + test "publish to url with with different ports" do + inbox80 = "http://42.site/users/nick1/inbox" + inbox42 = "http://42.site:42/users/nick1/inbox" + + mock(fn + %{method: :post, url: "http://42.site:42/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 42"}} + + %{method: :post, url: "http://42.site/users/nick1/inbox"} -> + {:ok, %Tesla.Env{status: 200, body: "port 80"}} + end) + + actor = insert(:user) + + assert {:ok, %{body: "port 42"}} = + Publisher.publish_one(%{ + inbox: inbox42, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + + assert {:ok, %{body: "port 80"}} = + Publisher.publish_one(%{ + inbox: inbox80, + json: "{}", + actor: actor, + id: 1, + unreachable_since: true + }) + end + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", Instances, [:passthrough], @@ -131,7 +164,6 @@ test "it returns inbox for messages involving single recipients in total" do inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) - assert called(Instances.set_reachable(inbox)) end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index b3b573c9b..9d657ac4f 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -7,8 +7,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do alias Pleroma.Activity alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.CommonAPI import ExUnit.CaptureLog import Pleroma.Factory @@ -53,8 +53,7 @@ test "returns errors when user not found" do test "returns activity" do user = insert(:user) service_actor = Relay.get_actor() - ActivityPub.follow(service_actor, user) - Pleroma.User.follow(service_actor, user) + CommonAPI.follow(service_actor, user) assert "#{user.ap_id}/followers" in User.following(service_actor) assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" @@ -74,6 +73,7 @@ test "returns error when activity not `Create` type" do assert Relay.publish(activity) == {:error, "Not implemented"} end + @tag capture_log: true test "returns error when activity not public" do activity = insert(:direct_note_activity) assert Relay.publish(activity) == {:error, false} diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 12c9ef1da..2649b060a 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -64,6 +64,47 @@ test "it streams out notifications and streams" do end end + describe "blocking users" do + setup do + user = insert(:user) + blocked = insert(:user) + User.follow(blocked, user) + User.follow(user, blocked) + + {:ok, block_data, []} = Builder.block(user, blocked) + {:ok, block, _meta} = ActivityPub.persist(block_data, local: true) + + %{user: user, blocked: blocked, block: block} + end + + test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + refute User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + + test "it blocks but does not unfollow if the relevant setting is set", %{ + user: user, + blocked: blocked, + block: block + } do + clear_config([:activitypub, :unfollow_blocked], false) + assert User.following?(user, blocked) + assert User.following?(blocked, user) + + {:ok, _, _} = SideEffects.handle(block) + + refute User.following?(user, blocked) + assert User.following?(blocked, user) + assert User.blocks?(user, blocked) + end + end + describe "update users" do setup do user = insert(:user) @@ -242,8 +283,7 @@ test "when activation is required", %{delete: delete, user: user} do {:ok, like} = CommonAPI.favorite(user, post.id) {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") {:ok, announce} = CommonAPI.repeat(post.id, user) - {:ok, block} = ActivityPub.block(user, poster) - User.block(user, poster) + {:ok, block} = CommonAPI.block(user, poster) {:ok, undo_data, _meta} = Builder.undo(user, like) {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) @@ -549,10 +589,29 @@ test "creates a notification", %{announce: announce, poster: poster} do end test "it streams out the announce", %{announce: announce} do - with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], stream_out: fn _ -> nil end do + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do {:ok, announce, _} = SideEffects.handle(announce) - assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(announce)) + assert called( + Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce) + ) + + assert called(Pleroma.Web.Push.send(:_)) end end end diff --git a/test/web/activity_pub/transmogrifier/block_handling_test.exs b/test/web/activity_pub/transmogrifier/block_handling_test.exs new file mode 100644 index 000000000..71f1a0ed5 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/block_handling_test.exs @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + test "it works for incoming blocks" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + blocker = insert(:user, ap_id: data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert User.blocks?(blocker, user) + end + + test "incoming blocks successfully tear down any follow relationship" do + blocker = insert(:user) + blocked = insert(:user) + + data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", blocked.ap_id) + |> Map.put("actor", blocker.ap_id) + + {:ok, blocker} = User.follow(blocker, blocked) + {:ok, blocked} = User.follow(blocked, blocker) + + assert User.following?(blocker, blocked) + assert User.following?(blocked, blocker) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Block" + assert data["object"] == blocked.ap_id + assert data["actor"] == blocker.ap_id + + blocker = User.get_cached_by_ap_id(data["actor"]) + blocked = User.get_cached_by_ap_id(data["object"]) + + assert User.blocks?(blocker, blocked) + + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + end +end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 06c39eed6..17e764ca1 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -160,7 +160,7 @@ test "it rejects incoming follow requests if the following errors for some reaso |> Poison.decode!() |> Map.put("object", user.ap_id) - with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do + with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) %Activity{} = activity = Activity.get_by_ap_id(id) diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 100821056..248b410c6 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do alias Pleroma.Object.Fetcher alias Pleroma.Tests.ObanHelpers alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI @@ -445,56 +444,6 @@ test "it works for incoming follows to locked account" do assert [^pending_follower] = User.get_follow_requests(user) end - test "it works for incoming blocks" do - user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - blocker = User.get_cached_by_ap_id(data["actor"]) - - assert User.blocks?(blocker, user) - end - - test "incoming blocks successfully tear down any follow relationship" do - blocker = insert(:user) - blocked = insert(:user) - - data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", blocked.ap_id) - |> Map.put("actor", blocker.ap_id) - - {:ok, blocker} = User.follow(blocker, blocked) - {:ok, blocked} = User.follow(blocked, blocker) - - assert User.following?(blocker, blocked) - assert User.following?(blocked, blocker) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Block" - assert data["object"] == blocked.ap_id - assert data["actor"] == blocker.ap_id - - blocker = User.get_cached_by_ap_id(data["actor"]) - blocked = User.get_cached_by_ap_id(data["object"]) - - assert User.blocks?(blocker, blocked) - - refute User.following?(blocker, blocked) - refute User.following?(blocked, blocker) - end - test "it works for incoming accepts which were pre-accepted" do follower = insert(:user) followed = insert(:user) @@ -502,7 +451,7 @@ test "it works for incoming accepts which were pre-accepted" do {:ok, follower} = User.follow(follower, followed) assert User.following?(follower, followed) == true - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -532,7 +481,7 @@ test "it works for incoming accepts which were orphaned" do follower = insert(:user) followed = insert(:user, locked: true) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -554,7 +503,7 @@ test "it works for incoming accepts which are referenced by IRI only" do follower = insert(:user) followed = insert(:user, locked: true) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -619,7 +568,7 @@ test "it works for incoming rejects which are orphaned" do followed = insert(:user, locked: true) {:ok, follower} = User.follow(follower, followed) - {:ok, _follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, followed) assert User.following?(follower, followed) == true @@ -645,7 +594,7 @@ test "it works for incoming rejects which are referenced by IRI only" do followed = insert(:user, locked: true) {:ok, follower} = User.follow(follower, followed) - {:ok, follow_activity} = ActivityPub.follow(follower, followed) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) assert User.following?(follower, followed) == true @@ -709,22 +658,44 @@ test "it remaps video URLs as attachments if necessary" do "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" ) - attachment = %{ - "type" => "Link", - "mediaType" => "video/mp4", - "url" => [ - %{ - "href" => - "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", - "mediaType" => "video/mp4" - } - ] - } - assert object.data["url"] == "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" - assert object.data["attachment"] == [attachment] + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4" + } + ] + } + ] + + {:ok, object} = + Fetcher.fetch_object_from_id( + "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" + ) + + assert object.data["attachment"] == [ + %{ + "type" => "Link", + "mediaType" => "video/mp4", + "url" => [ + %{ + "href" => + "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4", + "mediaType" => "video/mp4" + } + ] + } + ] + + assert object.data["url"] == + "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206" end test "it accepts Flag activities" do @@ -803,6 +774,29 @@ test "it correctly processes messages with non-array cc field" do assert [user.follower_address] == activity.data["to"] end + test "it correctly processes messages with weirdness in address fields" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => [nil, user.follower_address], + "cc" => ["https://www.w3.org/ns/activitystreams#Public", ["¿"]], + "type" => "Create", + "object" => %{ + "content" => "…", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end + test "it accepts Move activities" do old_user = insert(:user) new_user = insert(:user) diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index 15f03f193..361dc5a41 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI @@ -27,16 +26,6 @@ test "fetches the latest Follow activity" do end end - describe "fetch the latest Block" do - test "fetches the latest Block activity" do - blocker = insert(:user) - blocked = insert(:user) - {:ok, activity} = ActivityPub.block(blocker, blocked) - - assert activity == Utils.fetch_latest_block(blocker, blocked) - end - end - describe "determine_explicit_mentions()" do test "works with an object that has mentions" do object = %{ @@ -207,8 +196,8 @@ test "updates the state of all Follow activities with the same actor and object" user = insert(:user, locked: true) follower = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, user) - {:ok, follow_activity_two} = ActivityPub.follow(follower, user) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) data = follow_activity_two.data @@ -231,8 +220,8 @@ test "updates the state of the given follow activity" do user = insert(:user, locked: true) follower = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, user) - {:ok, follow_activity_two} = ActivityPub.follow(follower, user) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) data = follow_activity_two.data @@ -344,9 +333,9 @@ test "fetches last block activities" do user1 = insert(:user) user2 = insert(:user) - assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2) - assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2) - assert {:ok, %Activity{} = activity} = ActivityPub.block(user1, user2) + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2) + assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2) assert Utils.fetch_latest_block(user1, user2) == activity end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index bec15a996..98c7c9d09 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -158,4 +158,23 @@ test "sets correct totalItems when follows are hidden but the follow counter is assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) end end + + describe "acceptsChatMessages" do + test "it returns this value if it is set" do + true_user = insert(:user, accepts_chat_messages: true) + false_user = insert(:user, accepts_chat_messages: false) + nil_user = insert(:user, accepts_chat_messages: nil) + + assert %{"capabilities" => %{"acceptsChatMessages" => true}} = + UserView.render("user.json", user: true_user) + + assert %{"capabilities" => %{"acceptsChatMessages" => false}} = + UserView.render("user.json", user: false_user) + + refute Map.has_key?( + UserView.render("user.json", user: nil_user)["capabilities"], + "acceptsChatMessages" + ) + end + end end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index 48fb108ec..da91cd552 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -41,6 +41,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do {:ok, %{admin: admin, token: token, conn: conn}} end + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + user = insert(:user) + + conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") + + assert json_response(conn, 200) + end + describe "with [:auth, :enforce_oauth_admin_scope_usage]," do setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true) @@ -1514,6 +1524,15 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do end end + test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", + %{conn: conn} do + clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated) + user = insert(:user, %{local: false, nickname: "u@peer1.com"}) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") + + assert json_response(conn, 200) + end + describe "GET /users/:nickname/credentials" do test "gets the user credentials", %{conn: conn} do user = insert(:user) diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs index 064ef9bc7..61bc9fd39 100644 --- a/test/web/admin_api/controllers/config_controller_test.exs +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -152,6 +152,14 @@ test "subkeys with full update right merge", %{conn: conn} do assert emoji_val[:groups] == [a: 1, b: 2] assert assets_val[:mascots] == [a: 1, b: 2] end + + test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:admin_token], "password123") + + build_conn() + |> get("/api/pleroma/admin/config?admin_token=password123") + |> json_response_and_validate_schema(200) + end end test "POST /api/pleroma/admin/config error", %{conn: conn} do diff --git a/test/web/admin_api/controllers/report_controller_test.exs b/test/web/admin_api/controllers/report_controller_test.exs index 940bce340..f30dc8956 100644 --- a/test/web/admin_api/controllers/report_controller_test.exs +++ b/test/web/admin_api/controllers/report_controller_test.exs @@ -297,7 +297,7 @@ test "returns 403 when requested by a non-admin" do |> get("/api/pleroma/admin/reports") assert json_response(conn, :forbidden) == - %{"error" => "User is not an admin or OAuth admin scope is not granted."} + %{"error" => "User is not an admin."} end test "returns 403 when requested by anonymous" do diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 6bd26050e..7e11fede3 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -25,6 +25,52 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "blocking" do + setup do + blocker = insert(:user) + blocked = insert(:user) + User.follow(blocker, blocked) + User.follow(blocked, blocker) + %{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 {:ok, block} = CommonAPI.block(blocker, blocked) + + assert block.local + assert User.blocks?(blocker, blocked) + refute User.following?(blocker, blocked) + refute User.following?(blocked, blocker) + + assert called(Pleroma.Web.Federator.publish(block)) + end + 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(blocker, blocked) + + 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]) @@ -445,6 +491,7 @@ test "it filters out obviously bad tags when accepting a post as HTML" do object = Object.normalize(activity) assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["source"] == post end test "it filters out obviously bad tags when accepting a post as Markdown" do @@ -461,6 +508,7 @@ test "it filters out obviously bad tags when accepting a post as Markdown" do object = Object.normalize(activity) assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["source"] == post end test "it does not allow replies to direct messages that are not direct messages themselves" do @@ -886,6 +934,15 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do end end + describe "follow/2" do + test "directly follows a non-locked local user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + + assert User.following?(follower, followed) + end + end + describe "unfollow/2" do test "also unsubscribes a user" do [follower, followed] = insert_pair(:user) @@ -950,9 +1007,9 @@ test "after acceptance, it sets all existing pending follow request states to 'a follower = insert(:user) follower_two = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, user) - {:ok, follow_activity_two} = ActivityPub.follow(follower, user) - {:ok, follow_activity_three} = ActivityPub.follow(follower_two, user) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) assert follow_activity.data["state"] == "pending" assert follow_activity_two.data["state"] == "pending" @@ -970,9 +1027,9 @@ test "after rejection, it sets all existing pending follow request states to 're follower = insert(:user) follower_two = insert(:user) - {:ok, follow_activity} = ActivityPub.follow(follower, user) - {:ok, follow_activity_two} = ActivityPub.follow(follower, user) - {:ok, follow_activity_three} = ActivityPub.follow(follower_two, user) + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) + {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user) assert follow_activity.data["state"] == "pending" assert follow_activity_two.data["state"] == "pending" diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs index 3919ef93a..a65865860 100644 --- a/test/web/fallback_test.exs +++ b/test/web/fallback_test.exs @@ -6,22 +6,56 @@ defmodule Pleroma.Web.FallbackTest do use Pleroma.Web.ConnCase import Pleroma.Factory - test "GET /registration/:token", %{conn: conn} do - assert conn - |> get("/registration/foo") - |> html_response(200) =~ "" + describe "neither preloaded data nor metadata attached to" do + test "GET /registration/:token", %{conn: conn} do + response = get(conn, "/registration/foo") + + assert html_response(response, 200) =~ "" + end + + test "GET /*path", %{conn: conn} do + assert conn + |> get("/foo") + |> html_response(200) =~ "" + end end - test "GET /:maybe_nickname_or_id", %{conn: conn} do - user = insert(:user) + describe "preloaded data and metadata attached to" do + test "GET /:maybe_nickname_or_id", %{conn: conn} do + user = insert(:user) + user_missing = get(conn, "/foo") + user_present = get(conn, "/#{user.nickname}") - assert conn - |> get("/foo") - |> html_response(200) =~ "" + assert(html_response(user_missing, 200) =~ "") + refute html_response(user_present, 200) =~ "" + assert html_response(user_present, 200) =~ "initial-results" + end - refute conn - |> get("/" <> user.nickname) - |> html_response(200) =~ "" + test "GET /*path", %{conn: conn} do + assert conn + |> get("/foo") + |> html_response(200) =~ "" + + refute conn + |> get("/foo/bar") + |> html_response(200) =~ "" + end + end + + describe "preloaded data is attached to" do + test "GET /main/public", %{conn: conn} do + public_page = get(conn, "/main/public") + + refute html_response(public_page, 200) =~ "" + assert html_response(public_page, 200) =~ "initial-results" + end + + test "GET /main/all", %{conn: conn} do + public_page = get(conn, "/main/all") + + refute html_response(public_page, 200) =~ "" + assert html_response(public_page, 200) =~ "initial-results" + end end test "GET /api*path", %{conn: conn} do @@ -34,16 +68,6 @@ test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" end - test "GET /*path", %{conn: conn} do - assert conn - |> get("/foo") - |> html_response(200) =~ "" - - assert conn - |> get("/foo/bar") - |> html_response(200) =~ "" - end - test "OPTIONS /*path", %{conn: conn} do assert conn |> options("/foo") diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs index 1d107d56c..f3b54b5f2 100644 --- a/test/web/masto_fe_controller_test.exs +++ b/test/web/masto_fe_controller_test.exs @@ -24,7 +24,7 @@ test "put settings", %{conn: conn} do assert _result = json_response(conn, 200) user = User.get_cached_by_ap_id(user.ap_id) - assert user.settings == %{"programming" => "socks"} + assert user.mastofe_settings == %{"programming" => "socks"} end describe "index/2 redirections" do diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index f67d294ba..b888e4c71 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -108,6 +108,13 @@ test "updates the user's locking status", %{conn: conn} do assert user_data["locked"] == true end + test "updates the user's chat acceptance status", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["pleroma"]["accepts_chat_messages"] == false + end + test "updates the user's allow_following_move", %{user: user, conn: conn} do assert user.allow_following_move == true @@ -216,10 +223,21 @@ test "updates the user's avatar", %{user: user, conn: conn} do filename: "an_image.jpg" } - conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + assert user.avatar == %{} - assert user_response = json_response_and_validate_schema(conn, 200) + res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + + assert user_response = json_response_and_validate_schema(res, 200) assert user_response["avatar"] != User.avatar_url(user) + + user = User.get_by_id(user.id) + refute user.avatar == %{} + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""}) + + user = User.get_by_id(user.id) + assert user.avatar == nil end test "updates the user's banner", %{user: user, conn: conn} do @@ -229,26 +247,39 @@ test "updates the user's banner", %{user: user, conn: conn} do filename: "an_image.jpg" } - conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) + res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) - assert user_response = json_response_and_validate_schema(conn, 200) + assert user_response = json_response_and_validate_schema(res, 200) assert user_response["header"] != User.banner_url(user) + + # Also resets it + _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""}) + + user = User.get_by_id(user.id) + assert user.banner == nil end - test "updates the user's background", %{conn: conn} do + test "updates the user's background", %{conn: conn, user: user} do new_header = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - conn = + res = patch(conn, "/api/v1/accounts/update_credentials", %{ "pleroma_background_image" => new_header }) - assert user_response = json_response_and_validate_schema(conn, 200) + assert user_response = json_response_and_validate_schema(res, 200) assert user_response["pleroma"]["background_image"] + # + # Also resets it + _res = + patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""}) + + user = User.get_by_id(user.id) + assert user.background == nil end test "requires 'write:accounts' permission" do @@ -320,6 +351,30 @@ test "update fields", %{conn: conn} do ] end + test "emojis in fields labels", %{conn: conn} do + fields = [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + account_data = + conn + |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) + |> json_response_and_validate_schema(200) + + assert account_data["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert account_data["source"]["fields"] == [ + %{"name" => ":firefox:", "value" => "is best 2hu"}, + %{"name" => "they wins", "value" => ":blank:"} + ] + + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = account_data["emojis"] + end + test "update fields via x-www-form-urlencoded", %{conn: conn} do fields = [ diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index ebfcedd01..9c7b5e9b2 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -708,7 +708,10 @@ test "following without reblogs" do followed = insert(:user) other_user = insert(:user) - ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false") + ret_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200) @@ -722,7 +725,8 @@ test "following without reblogs" do assert %{"showing_reblogs" => true} = conn - |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true") + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) |> json_response_and_validate_schema(200) assert [%{"id" => ^reblog_id}] = @@ -731,6 +735,35 @@ test "following without reblogs" do |> json_response(200) end + test "following with reblogs" do + %{conn: conn} = oauth_access(["follow", "read:statuses"]) + followed = insert(:user) + other_user = insert(:user) + + ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow") + + assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed) + + assert [%{"id" => ^reblog_id}] = + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + + assert %{"showing_reblogs" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) + |> json_response_and_validate_schema(200) + + assert [] == + conn + |> get("/api/v1/timelines/home") + |> json_response(200) + end + test "following / unfollowing errors", %{user: user, conn: conn} do # self follow conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") @@ -780,7 +813,6 @@ test "with notifications", %{conn: conn} do assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = conn - |> put_req_header("content-type", "application/json") |> post("/api/v1/accounts/#{other_user.id}/mute") |> json_response_and_validate_schema(200) @@ -905,7 +937,7 @@ test "Account registration via Application", %{conn: conn} do %{ "access_token" => token, "created_at" => _created_at, - "scope" => _scope, + "scope" => ^scope, "token_type" => "Bearer" } = json_response_and_validate_schema(conn, 200) @@ -1067,7 +1099,7 @@ test "registration from trusted app" do assert %{ "access_token" => access_token, "created_at" => _, - "scope" => ["read", "write", "follow", "push"], + "scope" => "read write follow push", "token_type" => "Bearer" } = response @@ -1185,7 +1217,7 @@ test "creates an account and returns 200 if captcha is valid", %{conn: conn} do assert %{ "access_token" => access_token, "created_at" => _, - "scope" => ["read"], + "scope" => "read", "token_type" => "Bearer" } = conn diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index 44e12d15a..6749e0e83 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do use Pleroma.Web.ConnCase alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -20,7 +20,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do test "/api/v1/follow_requests works", %{user: user, conn: conn} do other_user = insert(:user) - {:ok, _activity} = ActivityPub.follow(other_user, user) + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) {:ok, other_user} = User.follow(other_user, user, :follow_pending) assert User.following?(other_user, user) == false @@ -34,7 +34,7 @@ test "/api/v1/follow_requests works", %{user: user, conn: conn} do test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do other_user = insert(:user) - {:ok, _activity} = ActivityPub.follow(other_user, user) + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) {:ok, other_user} = User.follow(other_user, user, :follow_pending) user = User.get_cached_by_id(user.id) @@ -56,7 +56,7 @@ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do other_user = insert(:user) - {:ok, _activity} = ActivityPub.follow(other_user, user) + {:ok, _, _, _activity} = CommonAPI.follow(other_user, user) user = User.get_cached_by_id(user.id) diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index 8bdfdddd1..cc880d82c 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -32,11 +32,15 @@ test "get instance information", %{conn: conn} do "avatar_upload_limit" => _, "background_upload_limit" => _, "banner_upload_limit" => _, - "background_image" => _ + "background_image" => _, + "chat_limit" => _, + "description_limit" => _ } = result + assert result["pleroma"]["metadata"]["account_activation_required"] != nil assert result["pleroma"]["metadata"]["features"] assert result["pleroma"]["metadata"]["federation"] + assert result["pleroma"]["metadata"]["fields_limits"] assert result["pleroma"]["vapid_public_key"] assert email == from_config_email diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 826f37fbc..24d1959f8 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -79,6 +79,7 @@ test "search", %{conn: conn} do assert status["id"] == to_string(activity.id) end + @tag capture_log: true test "constructs hashtags from search query", %{conn: conn} do results = conn @@ -318,11 +319,13 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do test "search fetches remote accounts", %{conn: conn} do user = insert(:user) + query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true}) + results = conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true") + |> get("/api/v1/search?#{query}") |> json_response_and_validate_schema(200) [account] = results["accounts"] diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index a98e939e8..d34f300da 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -22,6 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do setup do: clear_config([:instance, :federating]) setup do: clear_config([:instance, :allow_relay]) setup do: clear_config([:rich_media, :enabled]) + setup do: clear_config([:mrf, :policies]) + setup do: clear_config([:mrf_keyword, :reject]) describe "posting statuses" do setup do: oauth_access(["write:statuses"]) @@ -157,6 +159,17 @@ test "it fails to create a status if `expires_in` is less or equal than an hour" |> json_response_and_validate_schema(422) end + test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do + Pleroma.Config.put([:mrf_keyword, :reject], ["GNO"]) + Pleroma.Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + + assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) + |> json_response_and_validate_schema(422) + end + test "posting an undefined status with an attachment", %{user: user, conn: conn} do file = %Plug.Upload{ content_type: "image/jpg", @@ -760,13 +773,18 @@ test "if user is authenticated", %{local: local, remote: remote} do test "when you created it" do %{user: author, conn: conn} = oauth_access(["write:statuses"]) activity = insert(:note_activity, user: author) + object = Object.normalize(activity) - conn = + content = object.data["content"] + source = object.data["source"] + + result = conn |> assign(:user, author) |> delete("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) - assert %{} = json_response_and_validate_schema(conn, 200) + assert match?(%{"content" => ^content, "text" => ^source}, result) refute Activity.get_by_id(activity.id) end @@ -789,7 +807,7 @@ test "when you didn't create it" do conn = delete(conn, "/api/v1/statuses/#{activity.id}") - assert %{"error" => _} = json_response_and_validate_schema(conn, 403) + assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404) assert Activity.get_by_id(activity.id) == activity end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index f069390c1..50e0d783d 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -418,4 +418,78 @@ test "multi-hashtag timeline", %{conn: conn} do assert [status_none] == json_response_and_validate_schema(all_test, :ok) end end + + describe "hashtag timeline handling of :restrict_unauthenticated setting" do + setup do + user = insert(:user) + {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) + {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"}) + + activity1 + |> Ecto.Changeset.change(%{local: false}) + |> Pleroma.Repo.update() + + base_uri = "/api/v1/timelines/tag/tag1" + error_response = %{"error" => "authorization required for timeline view"} + + %{base_uri: base_uri, error_response: error_response} + end + + defp ensure_authenticated_access(base_uri) do + %{conn: auth_conn} = oauth_access(["read:statuses"]) + + res_conn = get(auth_conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(auth_conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + end + + test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{ + conn: conn, + base_uri: base_uri, + error_response: error_response + } do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + for local <- [true, false] do + res_conn = get(conn, "#{base_uri}?local=#{local}") + + assert json_response(res_conn, :unauthorized) == error_response + end + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], false) + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + res_conn = get(conn, "#{base_uri}?local=true") + assert length(json_response(res_conn, 200)) == 1 + + res_conn = get(conn, "#{base_uri}?local=false") + assert json_response(res_conn, :unauthorized) == error_response + + ensure_authenticated_access(base_uri) + end + + test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <> + "(but not to local public activities which are delivered as part of federated timeline)", + %{conn: conn, base_uri: base_uri, error_response: error_response} do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + clear_config([:restrict_unauthenticated, :timelines, :federated], false) + + res_conn = get(conn, "#{base_uri}?local=true") + assert json_response(res_conn, :unauthorized) == error_response + + # Note: local activities get delivered as part of federated timeline + res_conn = get(conn, "#{base_uri}?local=false") + assert length(json_response(res_conn, 200)) == 2 + + ensure_authenticated_access(base_uri) + end + end end diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs index a7f9c5205..c08be37d4 100644 --- a/test/web/mastodon_api/mastodon_api_test.exs +++ b/test/web/mastodon_api/mastodon_api_test.exs @@ -18,7 +18,7 @@ test "returns error when followed user is deactivated" do follower = insert(:user) user = insert(:user, local: true, deactivated: true) {:error, error} = MastodonAPI.follow(follower, user) - assert error == "Could not follow user: #{user.nickname} is deactivated." + assert error == :rejected end test "following for user" do diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 80b1f734c..a83bf90a3 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase + alias Pleroma.Config alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI @@ -18,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do :ok end + setup do: clear_config([:instances_favicons, :enabled]) + test "Represent a user account" do background_image = %{ "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] @@ -75,6 +78,8 @@ test "Represent a user account" do pleroma: %{ ap_id: user.ap_id, background_image: "https://example.com/images/asuka_hospital.png", + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", confirmation_pending: false, tags: [], is_admin: false, @@ -85,22 +90,37 @@ test "Represent a user account" do hide_followers_count: false, hide_follows_count: false, relationship: %{}, - skip_thread_containment: false + skip_thread_containment: false, + accepts_chat_messages: nil } } assert expected == AccountView.render("show.json", %{user: user}) end + test "Favicon is nil when :instances_favicons is disabled" do + user = insert(:user) + + Config.put([:instances_favicons, :enabled], true) + + assert %{ + pleroma: %{ + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" + } + } = AccountView.render("show.json", %{user: user}) + + Config.put([:instances_favicons, :enabled], false) + + assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user}) + end + test "Represent the user account for the account owner" do user = insert(:user) notification_settings = %{ - followers: true, - follows: true, - non_followers: true, - non_follows: true, - privacy_option: false + block_from_strangers: false, + hide_notification_contents: false } privacy = user.default_scope @@ -152,6 +172,8 @@ test "Represent a Service(bot) account" do pleroma: %{ ap_id: user.ap_id, background_image: nil, + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", confirmation_pending: false, tags: [], is_admin: false, @@ -162,7 +184,8 @@ test "Represent a Service(bot) account" do hide_followers_count: false, hide_follows_count: false, relationship: %{}, - skip_thread_containment: false + skip_thread_containment: false, + accepts_chat_messages: nil } } @@ -372,6 +395,9 @@ test "shows actual follower/following count to the account owner" do user = insert(:user, hide_followers: true, hide_follows: true) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + + assert User.following?(user, other_user) + assert Pleroma.FollowingRelationship.follower_count(other_user) == 1 {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{ diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 5cbadf0fc..fa26b3129 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -183,6 +183,7 @@ test "a note activity" do card: nil, reblog: nil, content: HTML.filter_tags(object_data["content"]), + text: nil, created_at: created_at, reblogs_count: 0, replies_count: 0, @@ -226,7 +227,8 @@ test "a note activity" do expires_at: nil, direct_conversation_id: nil, thread_muted: false, - emoji_reactions: [] + emoji_reactions: [], + parent_visible: false } } @@ -620,4 +622,20 @@ test "visibility/list" do assert status.visibility == "list" end + + test "has a field for parent visibility" do + user = insert(:user) + poster = insert(:user) + + {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"}) + + {:ok, visible} = + CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id}) + + status = StatusView.render("show.json", activity: visible, for: user) + refute status.pleroma.parent_visible + + status = StatusView.render("show.json", activity: visible, for: poster) + assert status.pleroma.parent_visible + end end diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs index d61cef83b..d4db44c63 100644 --- a/test/web/media_proxy/media_proxy_controller_test.exs +++ b/test/web/media_proxy/media_proxy_controller_test.exs @@ -4,82 +4,118 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do use Pleroma.Web.ConnCase - import Mock - alias Pleroma.Config - setup do: clear_config(:media_proxy) - setup do: clear_config([Pleroma.Web.Endpoint, :secret_key_base]) + import Mock + + alias Pleroma.Web.MediaProxy + alias Pleroma.Web.MediaProxy.MediaProxyController + alias Plug.Conn setup do on_exit(fn -> Cachex.clear(:banned_urls_cache) end) end test "it returns 404 when MediaProxy disabled", %{conn: conn} do - Config.put([:media_proxy, :enabled], false) + clear_config([:media_proxy, :enabled], false) - assert %Plug.Conn{ + assert %Conn{ status: 404, resp_body: "Not Found" } = get(conn, "/proxy/hhgfh/eeeee") - assert %Plug.Conn{ + assert %Conn{ status: 404, resp_body: "Not Found" } = get(conn, "/proxy/hhgfh/eeee/fff") end - test "it returns 403 when signature invalidated", %{conn: conn} do - Config.put([:media_proxy, :enabled], true) - Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - path = URI.parse(Pleroma.Web.MediaProxy.encode_url("https://google.fn")).path - Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + describe "" do + setup do + clear_config([:media_proxy, :enabled], true) + clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") + [url: MediaProxy.encode_url("https://google.fn/test.png")] + end - assert %Plug.Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, path) + test "it returns 403 for invalid signature", %{conn: conn, url: url} do + Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000") + %{path: path} = URI.parse(url) - assert %Plug.Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee") + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, path) - assert %Plug.Conn{ - status: 403, - resp_body: "Forbidden" - } = get(conn, "/proxy/hhgfh/eeee/fff") - end + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee") - test "redirects on valid url when filename invalidated", %{conn: conn} do - Config.put([:media_proxy, :enabled], true) - Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") - invalid_url = String.replace(url, "test.png", "test-file.png") - response = get(conn, invalid_url) - assert response.status == 302 - assert redirected_to(response) == url - end + assert %Conn{ + status: 403, + resp_body: "Forbidden" + } = get(conn, "/proxy/hhgfh/eeee/fff") + end - test "it performs ReverseProxy.call when signature valid", %{conn: conn} do - Config.put([:media_proxy, :enabled], true) - Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") + test "redirects on valid url when filename is invalidated", %{conn: conn, url: url} do + invalid_url = String.replace(url, "test.png", "test-file.png") + response = get(conn, invalid_url) + assert response.status == 302 + assert redirected_to(response) == url + end - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do - assert %Plug.Conn{status: :success} = get(conn, url) + test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: :success} = get(conn, url) + end + end + + test "it returns 404 when url is in banned_urls cache", %{conn: conn, url: url} do + MediaProxy.put_in_banned_urls("https://google.fn/test.png") + + with_mock Pleroma.ReverseProxy, + call: fn _conn, _url, _opts -> %Conn{status: :success} end do + assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) + end end end - test "it returns 404 when url contains in banned_urls cache", %{conn: conn} do - Config.put([:media_proxy, :enabled], true) - Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000") - url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png") - Pleroma.Web.MediaProxy.put_in_banned_urls("https://google.fn/test.png") + describe "filename_matches/3" do + test "preserves the encoded or decoded path" do + assert MediaProxyController.filename_matches( + %{"filename" => "/Hello world.jpg"}, + "/Hello world.jpg", + "http://pleroma.social/Hello world.jpg" + ) == :ok - with_mock Pleroma.ReverseProxy, - call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do - assert %Plug.Conn{status: 404, resp_body: "Not Found"} = get(conn, url) + assert MediaProxyController.filename_matches( + %{"filename" => "/Hello%20world.jpg"}, + "/Hello%20world.jpg", + "http://pleroma.social/Hello%20world.jpg" + ) == :ok + + assert MediaProxyController.filename_matches( + %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, + "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" + ) == :ok + + assert MediaProxyController.filename_matches( + %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, + "/my%2Flong%2Furl%2F2019%2F07%2FS.jp", + "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" + ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} + end + + test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do + # conn.request_path will return encoded url + request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" + + assert MediaProxyController.filename_matches( + true, + request_path, + "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" + ) == :ok end end end diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 69d2a71a6..72885cfdd 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -5,38 +5,33 @@ defmodule Pleroma.Web.MediaProxyTest do use ExUnit.Case use Pleroma.Tests.Helpers - import Pleroma.Web.MediaProxy - alias Pleroma.Web.MediaProxy.MediaProxyController - setup do: clear_config([:media_proxy, :enabled]) - setup do: clear_config(Pleroma.Upload) + alias Pleroma.Web.Endpoint + alias Pleroma.Web.MediaProxy describe "when enabled" do - setup do - Pleroma.Config.put([:media_proxy, :enabled], true) - :ok - end + setup do: clear_config([:media_proxy, :enabled], true) test "ignores invalid url" do - assert url(nil) == nil - assert url("") == nil + assert MediaProxy.url(nil) == nil + assert MediaProxy.url("") == nil end test "ignores relative url" do - assert url("/local") == "/local" - assert url("/") == "/" + assert MediaProxy.url("/local") == "/local" + assert MediaProxy.url("/") == "/" end test "ignores local url" do - local_url = Pleroma.Web.Endpoint.url() <> "/hello" - local_root = Pleroma.Web.Endpoint.url() - assert url(local_url) == local_url - assert url(local_root) == local_root + local_url = Endpoint.url() <> "/hello" + local_root = Endpoint.url() + assert MediaProxy.url(local_url) == local_url + assert MediaProxy.url(local_root) == local_root end test "encodes and decodes URL" do url = "https://pleroma.soykaf.com/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?( encoded, @@ -50,86 +45,44 @@ test "encodes and decodes URL" do test "encodes and decodes URL without a path" do url = "https://pleroma.soykaf.com" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end test "encodes and decodes URL without an extension" do url = "https://pleroma.soykaf.com/path/" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.ends_with?(encoded, "/path") assert decode_result(encoded) == url end test "encodes and decodes URL and ignores query params for the path" do url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.ends_with?(encoded, "/logo.png") assert decode_result(encoded) == url end test "validates signature" do - secret_key_base = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) + encoded = MediaProxy.url("https://pleroma.social") - on_exit(fn -> - Pleroma.Config.put([Pleroma.Web.Endpoint, :secret_key_base], secret_key_base) - end) - - encoded = url("https://pleroma.social") - - Pleroma.Config.put( - [Pleroma.Web.Endpoint, :secret_key_base], + clear_config( + [Endpoint, :secret_key_base], "00000000000000000000000000000000000000000000000" ) [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - assert decode_url(sig, base64) == {:error, :invalid_signature} - end - - test "filename_matches preserves the encoded or decoded path" do - assert MediaProxyController.filename_matches( - %{"filename" => "/Hello world.jpg"}, - "/Hello world.jpg", - "http://pleroma.social/Hello world.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/Hello%20world.jpg"}, - "/Hello%20world.jpg", - "http://pleroma.social/Hello%20world.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == :ok - - assert MediaProxyController.filename_matches( - %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"}, - "/my%2Flong%2Furl%2F2019%2F07%2FS.jp", - "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" - ) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"} - end - - test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do - # conn.request_path will return encoded url - request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg" - - assert MediaProxyController.filename_matches( - true, - request_path, - "https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg" - ) == :ok + assert MediaProxy.decode_url(sig, base64) == {:error, :invalid_signature} end test "uses the configured base_url" do - clear_config([:media_proxy, :base_url], "https://cache.pleroma.social") + base_url = "https://cache.pleroma.social" + clear_config([:media_proxy, :base_url], base_url) url = "https://pleroma.soykaf.com/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) - assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url])) + assert String.starts_with?(encoded, base_url) end # Some sites expect ASCII encoded characters in the URL to be preserved even if @@ -140,7 +93,7 @@ test "preserve ASCII encoding" do url = "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end @@ -151,56 +104,49 @@ test "preserve non-unicode characters per RFC3986" do url = "https://pleroma.com/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-._~:/?#[]@!$&'()*+,;=|^`{}" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end test "preserve unicode characters" do url = "https://ko.wikipedia.org/wiki/위키백과:대문" - encoded = url(url) + encoded = MediaProxy.url(url) assert decode_result(encoded) == url end end describe "when disabled" do - setup do - enabled = Pleroma.Config.get([:media_proxy, :enabled]) - - if enabled do - Pleroma.Config.put([:media_proxy, :enabled], false) - - on_exit(fn -> - Pleroma.Config.put([:media_proxy, :enabled], enabled) - :ok - end) - end - - :ok - end + setup do: clear_config([:media_proxy, :enabled], false) test "does not encode remote urls" do - assert url("https://google.fr") == "https://google.fr" + assert MediaProxy.url("https://google.fr") == "https://google.fr" end end defp decode_result(encoded) do [_, "proxy", sig, base64 | _] = URI.parse(encoded).path |> String.split("/") - {:ok, decoded} = decode_url(sig, base64) + {:ok, decoded} = MediaProxy.decode_url(sig, base64) decoded end describe "whitelist" do - setup do - Pleroma.Config.put([:media_proxy, :enabled], true) - :ok - end + setup do: clear_config([:media_proxy, :enabled], true) test "mediaproxy whitelist" do - Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"]) + clear_config([:media_proxy, :whitelist], ["https://google.com", "https://feld.me"]) url = "https://feld.me/foo.png" - unencoded = url(url) + unencoded = MediaProxy.url(url) + assert unencoded == url + end + + # TODO: delete after removing support bare domains for media proxy whitelist + test "mediaproxy whitelist bare domains whitelist (deprecated)" do + clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = MediaProxy.url(url) assert unencoded == url end @@ -211,17 +157,17 @@ test "does not change whitelisted urls" do media_url = "https://mycdn.akamai.com" url = "#{media_url}/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?(encoded, media_url) end test "ensure Pleroma.Upload base_url is always whitelisted" do media_url = "https://media.pleroma.social" - Pleroma.Config.put([Pleroma.Upload, :base_url], media_url) + clear_config([Pleroma.Upload, :base_url], media_url) url = "#{media_url}/static/logo.png" - encoded = url(url) + encoded = MediaProxy.url(url) assert String.starts_with?(encoded, media_url) end diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index 103997c31..07909d48b 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -13,8 +13,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do import Pleroma.Factory import Swoosh.TestAssertions - @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" - describe "POST /api/v1/pleroma/accounts/confirmation_resend" do setup do {:ok, user} = @@ -68,103 +66,6 @@ test "resend account confirmation email (with nickname)", %{conn: conn, user: us end end - describe "PATCH /api/v1/pleroma/accounts/update_avatar" do - setup do: oauth_access(["write:accounts"]) - - test "user avatar can be set", %{user: user, conn: conn} do - avatar_image = File.read!("test/fixtures/avatar_data_uri") - - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image}) - - user = refresh_record(user) - - assert %{ - "name" => _, - "type" => _, - "url" => [ - %{ - "href" => _, - "mediaType" => _, - "type" => _ - } - ] - } = user.avatar - - assert %{"url" => _} = json_response_and_validate_schema(conn, 200) - end - - test "user avatar can be reset", %{user: user, conn: conn} do - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""}) - - user = User.get_cached_by_id(user.id) - - assert user.avatar == nil - - assert %{"url" => nil} = json_response_and_validate_schema(conn, 200) - end - end - - describe "PATCH /api/v1/pleroma/accounts/update_banner" do - setup do: oauth_access(["write:accounts"]) - - test "can set profile banner", %{user: user, conn: conn} do - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image}) - - user = refresh_record(user) - assert user.banner["type"] == "Image" - - assert %{"url" => _} = json_response_and_validate_schema(conn, 200) - end - - test "can reset profile banner", %{user: user, conn: conn} do - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""}) - - user = refresh_record(user) - assert user.banner == %{} - - assert %{"url" => nil} = json_response_and_validate_schema(conn, 200) - end - end - - describe "PATCH /api/v1/pleroma/accounts/update_background" do - setup do: oauth_access(["write:accounts"]) - - test "background image can be set", %{user: user, conn: conn} do - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image}) - - user = refresh_record(user) - assert user.background["type"] == "Image" - # assert %{"url" => _} = json_response(conn, 200) - assert %{"url" => _} = json_response_and_validate_schema(conn, 200) - end - - test "background image can be reset", %{user: user, conn: conn} do - conn = - conn - |> put_req_header("content-type", "multipart/form-data") - |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""}) - - user = refresh_record(user) - assert user.background == %{} - assert %{"url" => nil} = json_response_and_validate_schema(conn, 200) - end - end - describe "getting favorites timeline of specified user" do setup do [current_user, user] = insert_pair(:user, hide_favorites: false) diff --git a/test/web/preload/instance_test.exs b/test/web/preload/instance_test.exs new file mode 100644 index 000000000..a46f28312 --- /dev/null +++ b/test/web/preload/instance_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.InstanceTest do + use Pleroma.DataCase + alias Pleroma.Web.Preload.Providers.Instance + + setup do: {:ok, Instance.generate_terms(nil)} + + test "it renders the info", %{"/api/v1/instance" => info} do + assert %{ + description: description, + email: "admin@example.com", + registrations: true + } = info + + assert String.equivalent?(description, "Pleroma: An efficient and flexible fediverse server") + end + + test "it renders the panel", %{"/instance/panel.html" => panel} do + assert String.contains?( + panel, + "

Welcome to Pleroma!

" + ) + end + + test "it works with overrides" do + clear_config([:instance, :static_dir], "test/fixtures/preload_static") + + %{"/instance/panel.html" => panel} = Instance.generate_terms(nil) + + assert String.contains?( + panel, + "HEY!" + ) + end + + test "it renders the node_info", %{"/nodeinfo/2.0.json" => nodeinfo} do + %{ + metadata: metadata, + version: "2.0" + } = nodeinfo + + assert metadata.private == false + assert metadata.suggestions == %{enabled: false} + end +end diff --git a/test/web/preload/timeline_test.exs b/test/web/preload/timeline_test.exs new file mode 100644 index 000000000..fea95a6a4 --- /dev/null +++ b/test/web/preload/timeline_test.exs @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.TimelineTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Preload.Providers.Timelines + + @public_url "/api/v1/timelines/public" + + describe "unauthenticated timeliness when restricted" do + setup do + svd_config = Pleroma.Config.get([:restrict_unauthenticated, :timelines]) + Pleroma.Config.put([:restrict_unauthenticated, :timelines], %{local: true, federated: true}) + + on_exit(fn -> + Pleroma.Config.put([:restrict_unauthenticated, :timelines], svd_config) + end) + + :ok + end + + test "return nothing" do + tl_data = Timelines.generate_terms(%{}) + + refute Map.has_key?(tl_data, "/api/v1/timelines/public") + end + end + + describe "unauthenticated timeliness when unrestricted" do + setup do + svd_config = Pleroma.Config.get([:restrict_unauthenticated, :timelines]) + + Pleroma.Config.put([:restrict_unauthenticated, :timelines], %{ + local: false, + federated: false + }) + + on_exit(fn -> + Pleroma.Config.put([:restrict_unauthenticated, :timelines], svd_config) + end) + + {:ok, user: insert(:user)} + end + + test "returns the timeline when not restricted" do + assert Timelines.generate_terms(%{}) + |> Map.has_key?(@public_url) + end + + test "returns public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 3 + end + + test "does not return non-public items", %{user: user} do + {:ok, _} = CommonAPI.post(user, %{status: "it's post 1!", visibility: "unlisted"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 2!", visibility: "direct"}) + {:ok, _} = CommonAPI.post(user, %{status: "it's post 3!"}) + + assert Timelines.generate_terms(%{}) + |> Map.fetch!(@public_url) + |> Enum.count() == 1 + end + end +end diff --git a/test/web/preload/user_test.exs b/test/web/preload/user_test.exs new file mode 100644 index 000000000..83f065e27 --- /dev/null +++ b/test/web/preload/user_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Preload.Providers.UserTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Preload.Providers.User + + describe "returns empty when user doesn't exist" do + test "nil user specified" do + assert User.generate_terms(%{user: nil}) == %{} + end + + test "missing user specified" do + assert User.generate_terms(%{user: :not_a_user}) == %{} + end + end + + describe "specified user exists" do + setup do + user = insert(:user) + + terms = User.generate_terms(%{user: user}) + %{terms: terms, user: user} + end + + test "account is rendered", %{terms: terms, user: user} do + account = terms["/api/v1/accounts/#{user.id}"] + assert %{acct: user, username: user} = account + end + end +end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index b48952b29..aeb5c1fbd 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -238,9 +238,11 @@ test "builds content for chat messages with no content" do } end - test "hides details for notifications when privacy option enabled" do + test "hides contents of notifications when option enabled" do user = insert(:user, nickname: "Bob") - user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) + + user2 = + insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: true}) {:ok, activity} = CommonAPI.post(user, %{ @@ -284,9 +286,11 @@ test "hides details for notifications when privacy option enabled" do } end - test "returns regular content for notifications with privacy option disabled" do + test "returns regular content when hiding contents option disabled" do user = insert(:user, nickname: "Bob") - user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false}) + + user2 = + insert(:user, nickname: "Rob", notification_settings: %{hide_notification_contents: false}) {:ok, activity} = CommonAPI.post(user, %{ diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs index a49ab002f..1598bf675 100644 --- a/test/web/static_fe/static_fe_controller_test.exs +++ b/test/web/static_fe/static_fe_controller_test.exs @@ -87,6 +87,20 @@ test "single notice page", %{conn: conn, user: user} do 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: ""}) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 245f6e63f..d56d74464 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -116,6 +116,35 @@ test "it streams boosts of the user in the 'user' stream", %{user: user} do refute Streamer.filtered_by_user?(user, announce) end + test "it does not stream announces of the user's own posts in the 'user' stream", %{ + user: user + } do + Streamer.get_topic_and_add_socket("user", user) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + assert Streamer.filtered_by_user?(user, announce) + end + + test "it does stream notifications announces of the user's own posts in the 'user' stream", %{ + user: user + } do + Streamer.get_topic_and_add_socket("user", user) + + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, announce} = CommonAPI.repeat(activity.id, other_user) + + notification = + Pleroma.Notification + |> Repo.get_by(%{user_id: user.id, activity_id: announce.id}) + |> Repo.preload(:activity) + + refute Streamer.filtered_by_user?(user, notification) + end + test "it streams boosts of mastodon user in the 'user' stream", %{user: user} do Streamer.get_topic_and_add_socket("user", user) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index ad919d341..109c1e637 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -191,7 +191,7 @@ test "it imports blocks with different nickname variations", %{conn: conn} do test "it updates notification settings", %{user: user, conn: conn} do conn |> put("/api/pleroma/notification_settings", %{ - "followers" => false, + "block_from_strangers" => true, "bar" => 1 }) |> json_response(:ok) @@ -199,130 +199,25 @@ test "it updates notification settings", %{user: user, conn: conn} do user = refresh_record(user) assert %Pleroma.User.NotificationSetting{ - followers: false, - follows: true, - non_follows: true, - non_followers: true, - privacy_option: false + block_from_strangers: true, + hide_notification_contents: false } == user.notification_settings end - test "it updates notification privacy option", %{user: user, conn: conn} do + test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do conn - |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"}) + |> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"}) |> json_response(:ok) user = refresh_record(user) assert %Pleroma.User.NotificationSetting{ - followers: true, - follows: true, - non_follows: true, - non_followers: true, - privacy_option: true + block_from_strangers: false, + hide_notification_contents: true } == user.notification_settings end end - describe "GET /api/statusnet/config" do - test "it returns config in xml format", %{conn: conn} do - instance = Config.get(:instance) - - response = - conn - |> put_req_header("accept", "application/xml") - |> get("/api/statusnet/config") - |> response(:ok) - - assert response == - "\n\n#{Keyword.get(instance, :name)}\n#{ - Pleroma.Web.base_url() - }\n#{Keyword.get(instance, :limit)}\n#{ - !Keyword.get(instance, :registrations_open) - }\n\n\n" - end - - test "it returns config in json format", %{conn: conn} do - instance = Config.get(:instance) - Config.put([:instance, :managed_config], true) - Config.put([:instance, :registrations_open], false) - Config.put([:instance, :invites_enabled], true) - Config.put([:instance, :public], false) - Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) - - response = - conn - |> put_req_header("accept", "application/json") - |> get("/api/statusnet/config") - |> json_response(:ok) - - expected_data = %{ - "site" => %{ - "accountActivationRequired" => "0", - "closed" => "1", - "description" => Keyword.get(instance, :description), - "invitesEnabled" => "1", - "name" => Keyword.get(instance, :name), - "pleromafe" => %{"theme" => "asuka-hospital"}, - "private" => "1", - "safeDMMentionsEnabled" => "0", - "server" => Pleroma.Web.base_url(), - "textlimit" => to_string(Keyword.get(instance, :limit)), - "uploadlimit" => %{ - "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)), - "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)), - "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)), - "uploadlimit" => to_string(Keyword.get(instance, :upload_limit)) - }, - "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } - } - - assert response == expected_data - end - - test "returns the state of safe_dm_mentions flag", %{conn: conn} do - Config.put([:instance, :safe_dm_mentions], true) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["safeDMMentionsEnabled"] == "1" - - Config.put([:instance, :safe_dm_mentions], false) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["safeDMMentionsEnabled"] == "0" - end - - test "it returns the managed config", %{conn: conn} do - Config.put([:instance, :managed_config], false) - Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - refute response["site"]["pleromafe"] - - Config.put([:instance, :managed_config], true) - - response = - conn - |> get("/api/statusnet/config.json") - |> json_response(:ok) - - assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"} - end - end - describe "GET /api/pleroma/frontend_configurations" do test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do config = [ @@ -451,28 +346,6 @@ test "with valid permissions and invalid password, it returns an error", %{conn: end end - describe "GET /api/statusnet/version" do - test "it returns version in xml format", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/xml") - |> get("/api/statusnet/version") - |> response(:ok) - - assert response == "#{Pleroma.Application.named_version()}" - end - - test "it returns version in json format", %{conn: conn} do - response = - conn - |> put_req_header("accept", "application/json") - |> get("/api/statusnet/version") - |> json_response(:ok) - - assert response == "#{Pleroma.Application.named_version()}" - end - end - describe "POST /main/ostatus - remote_subscribe/2" do setup do: clear_config([:instance, :federating], true) diff --git a/test/workers/cron/clear_oauth_token_worker_test.exs b/test/workers/cron/clear_oauth_token_worker_test.exs index df82dc75d..67836f34f 100644 --- a/test/workers/cron/clear_oauth_token_worker_test.exs +++ b/test/workers/cron/clear_oauth_token_worker_test.exs @@ -16,7 +16,7 @@ test "deletes expired tokens" do ) Pleroma.Config.put([:oauth2, :clean_expired_tokens], true) - ClearOauthTokenWorker.perform(:opts, :job) + ClearOauthTokenWorker.perform(%Oban.Job{}) assert Pleroma.Repo.all(Pleroma.Web.OAuth.Token) == [] end end diff --git a/test/workers/cron/digest_emails_worker_test.exs b/test/workers/cron/digest_emails_worker_test.exs index f9bc50db5..65887192e 100644 --- a/test/workers/cron/digest_emails_worker_test.exs +++ b/test/workers/cron/digest_emails_worker_test.exs @@ -35,7 +35,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do end test "it sends digest emails", %{user2: user2} do - Pleroma.Workers.Cron.DigestEmailsWorker.perform(:opts, :pid) + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) # Performing job(s) enqueued at previous step ObanHelpers.perform_all() @@ -47,7 +47,7 @@ test "it sends digest emails", %{user2: user2} do test "it doesn't fail when a user has no email", %{user2: user2} do {:ok, _} = user2 |> Ecto.Changeset.change(%{email: nil}) |> Pleroma.Repo.update() - Pleroma.Workers.Cron.DigestEmailsWorker.perform(:opts, :pid) + Pleroma.Workers.Cron.DigestEmailsWorker.perform(%Oban.Job{}) # Performing job(s) enqueued at previous step ObanHelpers.perform_all() end diff --git a/test/workers/cron/new_users_digest_worker_test.exs b/test/workers/cron/new_users_digest_worker_test.exs index ee589bb55..129534cb1 100644 --- a/test/workers/cron/new_users_digest_worker_test.exs +++ b/test/workers/cron/new_users_digest_worker_test.exs @@ -17,7 +17,7 @@ test "it sends new users digest emails" do user2 = insert(:user, %{inserted_at: yesterday}) CommonAPI.post(user, %{status: "cofe"}) - NewUsersDigestWorker.perform(nil, nil) + NewUsersDigestWorker.perform(%Oban.Job{}) ObanHelpers.perform_all() assert_received {:email, email} @@ -39,7 +39,7 @@ test "it doesn't fail when admin has no email" do CommonAPI.post(user, %{status: "cofe"}) - NewUsersDigestWorker.perform(nil, nil) + NewUsersDigestWorker.perform(%Oban.Job{}) ObanHelpers.perform_all() end end diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index b1db59fdf..d1acd9ae6 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -32,7 +32,7 @@ test "deletes an expiration activity" do %{activity_id: activity.id, scheduled_at: naive_datetime} ) - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid) + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) refute Pleroma.Repo.get(Pleroma.Activity, activity.id) refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) @@ -58,7 +58,7 @@ test "works with ActivityExpirationPolicy" do |> Ecto.Changeset.change(%{scheduled_at: past_date}) |> Repo.update!() - Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid) + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(%Oban.Job{}) assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] = Pleroma.Repo.all(Pleroma.Activity) diff --git a/test/workers/scheduled_activity_worker_test.exs b/test/workers/scheduled_activity_worker_test.exs index b312d975b..f3eddf7b1 100644 --- a/test/workers/scheduled_activity_worker_test.exs +++ b/test/workers/scheduled_activity_worker_test.exs @@ -32,10 +32,7 @@ test "creates a status from the scheduled activity" do params: %{status: "hi"} ) - ScheduledActivityWorker.perform( - %{"activity_id" => scheduled_activity.id}, - :pid - ) + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => scheduled_activity.id}}) refute Repo.get(ScheduledActivity, scheduled_activity.id) activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) @@ -46,7 +43,7 @@ test "adds log message if ScheduledActivity isn't find" do Pleroma.Config.put([ScheduledActivity, :enabled], true) assert capture_log([level: :error], fn -> - ScheduledActivityWorker.perform(%{"activity_id" => 42}, :pid) + ScheduledActivityWorker.perform(%Oban.Job{args: %{"activity_id" => 42}}) end) =~ "Couldn't find scheduled activity" end end