diff --git a/.gitignore b/.gitignore index f30f4cf5f..da73b6f36 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ erl_crash.dump # variables. /config/*.secret.exs /config/generated_config.exs +/config/runtime.exs /config/*.env @@ -56,4 +57,4 @@ pleroma.iml # Editor temp files /*~ -/*# \ No newline at end of file +/*# diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78e715d47..844f5888e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,7 @@ stages: - docker before_script: + - echo $MIX_ENV - rm -rf _build/*/lib/pleroma - apt-get update && apt-get install -y cmake - mix local.hex --force @@ -37,11 +38,20 @@ after_script: build: stage: build + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" script: - mix compile --force spec-build: stage: test + only: + changes: + - "lib/pleroma/web/api_spec/**/*.ex" + - "lib/pleroma/web/api_spec.ex" artifacts: paths: - spec.json @@ -64,6 +74,11 @@ benchmark: unit-testing: stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" retry: 2 cache: &testing_cache_policy <<: *global_cache_policy @@ -97,6 +112,11 @@ unit-testing: unit-testing-rum: stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" retry: 2 cache: *testing_cache_policy services: @@ -114,17 +134,42 @@ unit-testing-rum: - mix test --preload-modules lint: + image: elixir:1.12 stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" cache: *testing_cache_policy script: - mix format --check-formatted analysis: stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" cache: *testing_cache_policy script: - mix credo --strict --only=warnings,todo,fixme,consistency,readability +cycles: + stage: test + image: elixir:1.11 + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" + cache: {} + script: + - mix deps.get + - mix compile + - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}' + docs-deploy: stage: deploy cache: *testing_cache_policy @@ -199,7 +244,7 @@ stop_review_app: amd64: stage: release - image: elixir:1.10.3 + image: elixir:1.10.4 only: &release-only - stable@pleroma/pleroma - develop@pleroma/pleroma @@ -237,7 +282,7 @@ amd64-musl: stage: release artifacts: *release-artifacts only: *release-only - image: elixir:1.10.3-alpine + image: elixir:1.10.4-alpine cache: *release-cache variables: *release-variables before_script: &before-release-musl @@ -253,7 +298,7 @@ arm: only: *release-only tags: - arm32-specified - image: arm32v7/elixir:1.10.3 + image: arm32v7/elixir:1.10.4 cache: *release-cache variables: *release-variables before_script: *before-release @@ -265,7 +310,7 @@ arm-musl: only: *release-only tags: - arm32-specified - image: arm32v7/elixir:1.10.3-alpine + image: arm32v7/elixir:1.10.4-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl @@ -277,7 +322,7 @@ arm64: only: *release-only tags: - arm - image: arm64v8/elixir:1.10.3 + image: arm64v8/elixir:1.10.4 cache: *release-cache variables: *release-variables before_script: *before-release @@ -289,7 +334,7 @@ arm64-musl: only: *release-only tags: - arm - image: arm64v8/elixir:1.10.3-alpine + image: arm64v8/elixir:1.10.4-alpine cache: *release-cache variables: *release-variables before_script: *before-release-musl @@ -307,8 +352,8 @@ docker: IMAGE_TAG_SLUG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG IMAGE_TAG_LATEST: $CI_REGISTRY_IMAGE:latest IMAGE_TAG_LATEST_STABLE: $CI_REGISTRY_IMAGE:latest-stable - DOCKER_BUILDX_URL: https://github.com/docker/buildx/releases/download/v0.4.1/buildx-v0.4.1.linux-amd64 - DOCKER_BUILDX_HASH: 71a7d01439aa8c165a25b59c44d3f016fddbd98b + DOCKER_BUILDX_URL: https://github.com/docker/buildx/releases/download/v0.6.3/buildx-v0.6.3.linux-amd64 + DOCKER_BUILDX_HASH: 980e6b9655f971991fbbb5fd6cd19f1672386195 before_script: &before-docker - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker pull $IMAGE_TAG_SLUG || true diff --git a/CHANGELOG.md b/CHANGELOG.md index 625cf3266..ee9e04568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,32 +6,90 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Removed + +- MastoFE + +### Changed +- Allow users to remove their emails if instance does not need email to register + +### Added +- `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object + +### Fixed +- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies +- Handle Reject for already-accepted Follows properly +- Display OpenGraph data on alternative notice routes. + +### Removed + +## 2.4.1 - 2021-08-29 + +### Changed +- Make `mix pleroma.database set_text_search_config` run concurrently and indefinitely + +### Added +- AdminAPI: Missing configuration description for StealEmojiPolicy + +### Fixed +- MastodonAPI: Stream out Create activities +- MRF ObjectAgePolicy: Fix pattern matching on "published" +- TwitterAPI: Make `change_password` and `change_email` require params on body instead of query +- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies +- AdminAPI: Fix rendering reports containing a `nil` object +- Mastodon API: Activity Search fallbacks on status fetching after a DB Timeout/Error +- Mastodon API: Fix crash in Streamer related to reblogging +- AdminAPI: List available frontends when `static/frontends` folder is missing +- Make activity search properly use language-aware GIN indexes +- AdminAPI: Fix suggestions for MRF Policies + +## 2.4.0 - 2021-08-08 + ### Changed +- **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit` +- **Breaking** Entries for simple_policy, transparency_exclusions and quarantined_instances now list both the instance and a reason. +- Support for Erlang/OTP 24 - The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change. - HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising. +- Email address is now returned if requesting user is the owner of the user account so it can be exposed in client and FE user settings UIs. +- Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available. +- AdminAPI: sort users so the newest are at the top. +- ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators ### Added - MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded. - Return OAuth token `id` (primary key) in POST `/oauth/token`. +- AdminAPI: return `created_at` date with users. +- AdminAPI: add DELETE `/api/v1/pleroma/admin/instances/:instance` to delete all content from a remote instance. +- `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time. +- Attachment dimensions and blurhashes are federated when available. +- Mastodon API: support `poll` notification. +- Pinned posts federation +- AdminAPI: allow moderators to manage reports, users, invites, and custom emojis ### Fixed - Don't crash so hard when email settings are invalid. -- Display OpenGraph data on alternative notice routes. - -## Unreleased (Patch) - -### Fixed - +- Checking activated Upload Filters for required commands. +- Remote users can no longer reappear after being deleted. +- Deactivated users may now be deleted. +- Deleting an activity with a lot of likes/boosts no longer causes a database timeout. +- Mix task `pleroma.database prune_objects` +- Fixed rendering of JSON errors on ActivityPub endpoints. +- Linkify: Parsing crash with URLs ending in unbalanced closed paren, no path separator, and no query parameters - Try to save exported ConfigDB settings (migrate_from_db) in the system temp directory if default location is not writable. - Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image - Applying ConcurrentLimiter settings via AdminAPI - User login failures if their `notification_settings` were in a NULL state. - Mix task `pleroma.user delete_activities` query transaction timeout is now :infinity +- MRF (`SimplePolicy`): Embedded objects are now checked. If any embedded object would be rejected, its parent is rejected. This fixes Announces leaking posts from blocked domains. - Fixed some Markdown issues, including trailing slash in links. -## [2.3.0] - 2020-03-01 +### Removed +- **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`) + +## [2.3.0] - 2021-03-01 ### Security @@ -81,6 +139,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support pagination of blocks and mutes. - Account backup. - Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance. +- `[:activitypub, :blockers_visible]` config to control visibility of blockers. - Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`. - The site title is now injected as a `title` tag like preloads or metadata. - Password reset tokens now are not accepted after a certain age. @@ -132,7 +191,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Support for expires_in/expires_at in the Filters. -## [2.2.2] - 2020-01-18 +## [2.2.2] - 2021-01-18 ### Fixed diff --git a/Dockerfile b/Dockerfile index b1b5171af..c51ebbab0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev &&\ mkdir release &&\ mix release --path release -FROM alpine:3.11 +FROM alpine:3.14 ARG BUILD_DATE ARG VCS_REF @@ -31,9 +31,8 @@ LABEL maintainer="ops@pleroma.social" \ ARG HOME=/opt/pleroma ARG DATA=/var/lib/pleroma -RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ - apk update &&\ - apk add exiftool imagemagick libmagic ncurses postgresql-client &&\ +RUN apk update &&\ + apk add exiftool ffmpeg imagemagick libmagic ncurses postgresql-client &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\ mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/static &&\ diff --git a/README.md b/README.md index 7a05b9e48..25fde90b9 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,14 @@ If your platform is not supported, or you just want to be able to edit the sourc - [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/) ### OS/Distro packages -Currently Pleroma is not packaged by any OS/Distros, but if you want to package it for one, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**. +Currently Pleroma is packaged for [YunoHost](https://yunohost.org). If you want to package Pleroma for any OS/Distros, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**. ### Docker While we don’t provide docker files, other people have written very good ones. Take a look at or . +### Raspberry Pi +Community maintained Raspberry Pi image that you can flash and run Pleroma on your Raspberry Pi. Available here . + ### Compilation Troubleshooting If you ever encounter compilation issues during the updating of Pleroma, you can try these commands and see if they fix things: @@ -50,5 +53,5 @@ If you are not developing Pleroma, it is better to use the OTP release, which co - Latest Git revision: ## Community Channels -* IRC: **#pleroma** and **#pleroma-dev** on freenode, webchat is available at -* Matrix: and +* IRC: **#pleroma** and **#pleroma-dev** on libera.chat, webchat is available at +* Matrix: [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) and [#pleroma-dev:libera.chat](https://matrix.to/#/#pleroma-dev:libera.chat) diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex index f5c7bfce8..7f262d228 100644 --- a/benchmarks/load_testing/activities.ex +++ b/benchmarks/load_testing/activities.ex @@ -299,7 +299,7 @@ defp insert_activity(:attachment, visibility, group, users, _opts) do "url" => [ %{ "href" => - "#{Pleroma.Web.base_url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg", + "#{Pleroma.Web.Endpoint.url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg", "mediaType" => "image/jpeg", "type" => "Link" } @@ -394,7 +394,7 @@ defp get_actor(group, users), do: Enum.random(users[group]) defp other_data(actor, content) do %{host: host} = URI.parse(actor.ap_id) - datetime = DateTime.utc_now() + datetime = DateTime.utc_now() |> to_string() context_id = "https://#{host}/contexts/#{UUID.generate()}" activity_id = "https://#{host}/activities/#{UUID.generate()}" object_id = "https://#{host}/objects/#{UUID.generate()}" diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index c051335a5..a32de2db4 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -99,15 +99,16 @@ defp hashtag_fetching(params, user, local_only) do |> Enum.map(&String.downcase(&1)) _activities = - params - |> Map.put(:type, "Create") - |> Map.put(:local_only, local_only) - |> Map.put(:blocking_user, user) - |> Map.put(:muting_user, user) - |> Map.put(:user, user) - |> Map.put(:tag, tags) - |> Map.put(:tag_all, tag_all) - |> Map.put(:tag_reject, tag_reject) + %{ + type: "Create", + local_only: local_only, + blocking_user: user, + muting_user: user, + user: user, + tag: tags, + tag_all: tag_all, + tag_reject: tag_reject, + } |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() end end diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex index aed32f194..3770ca163 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex @@ -17,14 +17,14 @@ def run(_args) do # Let the user make 100 posts 1..100 - |> Enum.each(fn i -> CommonAPI.post(user, %{"status" => to_string(i)}) end) + |> Enum.each(fn i -> CommonAPI.post(user, %{status: to_string(i)}) end) # Let 10 random users post posts = users |> Enum.take_random(10) |> Enum.map(fn {:ok, random_user} -> - {:ok, activity} = CommonAPI.post(random_user, %{"status" => "."}) + {:ok, activity} = CommonAPI.post(random_user, %{status: "."}) activity end) @@ -42,7 +42,7 @@ def run(_args) do |> Conn.assign(:user, reading_user) |> Conn.assign(:skip_link_headers, true) - Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) + Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{id: user.id}) end }, inputs: %{"user" => user, "no user" => nil}, @@ -50,7 +50,7 @@ def run(_args) do ) users - |> Enum.each(fn {:ok, follower, user} -> Pleroma.User.follow(follower, user) end) + |> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end) Benchee.run( %{ @@ -60,7 +60,7 @@ def run(_args) do |> Conn.assign(:user, reading_user) |> Conn.assign(:skip_link_headers, true) - Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id}) + Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{id: user.id}) end }, inputs: %{"user" => user, "no user" => nil}, diff --git a/config/benchmark.exs b/config/benchmark.exs index 5567ff26e..9a7ea5669 100644 --- a/config/benchmark.exs +++ b/config/benchmark.exs @@ -1,11 +1,10 @@ -use Mix.Config +import Config # We don't run a server during test. If one is required, # you can enable the server option below. config :pleroma, Pleroma.Web.Endpoint, http: [port: 4001], - url: [port: 4001], - server: true + url: [port: 4001] # Disable captha for tests config :pleroma, Pleroma.Captcha, @@ -44,7 +43,7 @@ pool_size: 10 # Reduce hash rounds for testing -config :pbkdf2_elixir, rounds: 1 +config :pleroma, :password, iterations: 1 config :tesla, adapter: Tesla.Mock diff --git a/config/config.exs b/config/config.exs index 4381068ac..c9592511f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -41,7 +41,7 @@ # # This configuration file is loaded before any dependency and # is restricted to this project. -use Mix.Config +import Config # General application configuration config :pleroma, ecto_repos: [Pleroma.Repo] @@ -139,6 +139,7 @@ ], protocol: "https", secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl", + live_view: [signing_salt: "U5ELgdEwTD3n1+D5s0rY0AMg8/y1STxZ3Zvsl3bWh+oBcGrYdil0rXqPMRd3Glcq"], signing_salt: "CqaoopA2", render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)], pubsub_server: Pleroma.PubSub, @@ -190,7 +191,6 @@ 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, avatar_upload_limit: 2_000_000, @@ -322,9 +322,6 @@ subjectLineBehavior: "email", theme: "pleroma-dark", webPushNotifications: false - }, - masto_fe: %{ - showInstanceSpecificPanel: true } config :pleroma, :assets, @@ -353,6 +350,7 @@ config :pleroma, :activitypub, unfollow_blocked: true, outgoing_blocks: true, + blockers_visible: true, follow_handshake_timeout: 500, note_replies_output_limit: 5, sign_object_fetches: true, @@ -457,9 +455,11 @@ image_quality: 85, min_content_length: 100 * 1024 -config :pleroma, :chat, enabled: true +config :pleroma, :shout, + enabled: true, + limit: 5_000 -config :phoenix, :format_encoders, json: Jason +config :phoenix, :format_encoders, json: Jason, "activity+json": Jason config :phoenix, :json_library, Jason @@ -559,6 +559,7 @@ mailer: 10, transmogrifier: 20, scheduled_activities: 10, + poll_notifications: 10, background: 5, remote_fetcher: 2, attachments_cleanup: 1, diff --git a/config/description.exs b/config/description.exs index bb1f43305..1c8c3b4a0 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config websocket_config = [ path: "/websocket", @@ -544,14 +544,6 @@ 5_000 ] }, - %{ - key: :chat_limit, - type: :integer, - description: "Character limit of the instance chat messages", - suggestions: [ - 5_000 - ] - }, %{ key: :remote_limit, type: :integer, @@ -682,7 +674,8 @@ %{ key: :allow_relay, type: :boolean, - description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance" + description: + "Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance." }, %{ key: :public, @@ -694,12 +687,14 @@ }, %{ key: :quarantined_instances, - type: {:list, :string}, + type: {:list, :tuple}, + key_placeholder: "instance", + value_placeholder: "reason", description: - "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent", + "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent and the reason for doing so", suggestions: [ - "quarantined.com", - "*.quarantined.com" + {"quarantined.com", "Reason"}, + {"*.quarantined.com", "Reason"} ] }, %{ @@ -1169,7 +1164,7 @@ type: :group, description: "This form can be used to configure a keyword list that keeps the configuration data for any " <> - "kind of frontend. By default, settings for pleroma_fe and masto_fe are configured. If you want to " <> + "kind of frontend. By default, settings for pleroma_fe are configured. If you want to " <> "add your own configuration your settings all fields must be complete.", children: [ %{ @@ -1182,7 +1177,6 @@ alwaysShowSubjectInput: true, background: "/static/aurora_borealis.jpg", collapseMessageWithSubject: false, - disableChat: false, greentext: false, hideFilteredStatuses: false, hideMutedPosts: false, @@ -1229,12 +1223,6 @@ description: "When a message has a subject (aka Content Warning), collapse it by default" }, - %{ - key: :disableChat, - label: "PleromaFE Chat", - type: :boolean, - description: "Disables PleromaFE Chat component" - }, %{ key: :greentext, label: "Greentext", @@ -1376,25 +1364,6 @@ suggestions: ["pleroma-dark"] } ] - }, - %{ - key: :masto_fe, - label: "Masto FE", - type: :map, - description: "Settings for Masto FE", - suggestions: [ - %{ - showInstanceSpecificPanel: true - } - ], - children: [ - %{ - key: :showInstanceSpecificPanel, - label: "Show instance specific panel", - type: :boolean, - description: "Whenether to show the instance's specific panel" - } - ] } ] }, @@ -1701,6 +1670,11 @@ type: :boolean, description: "Whether to federate blocks to other instances" }, + %{ + key: :blockers_visible, + type: :boolean, + description: "Whether a user can see someone who has blocked them" + }, %{ key: :sign_object_fetches, type: :boolean, @@ -2652,13 +2626,22 @@ }, %{ group: :pleroma, - key: :chat, + key: :shout, type: :group, - description: "Pleroma chat settings", + description: "Pleroma shout settings", children: [ %{ key: :enabled, - type: :boolean + type: :boolean, + description: "Enables the backend Shoutbox chat feature." + }, + %{ + key: :limit, + type: :integer, + description: "Shout message character limit.", + suggestions: [ + 5_000 + ] } ] }, diff --git a/config/dev.exs b/config/dev.exs index 4faaeff5b..ab3e83c12 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For development, we disable any cache and enable # debugging and code reloading. @@ -54,10 +54,15 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true +# Reduce recompilation time +# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects +config :phoenix, :plug_init_mode, :runtime + if File.exists?("./config/dev.secret.exs") do import_config "dev.secret.exs" else IO.puts( + :stderr, "!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs" ) end diff --git a/config/dokku.exs b/config/dokku.exs index 9ea0ec450..1cc396c3d 100644 --- a/config/dokku.exs +++ b/config/dokku.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :pleroma, Pleroma.Web.Endpoint, http: [ diff --git a/config/prod.exs b/config/prod.exs index adbce5606..968f596e0 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # For production, we often load configuration from external # sources, such as your system environment. For this reason, @@ -63,7 +63,12 @@ # Finally import the config/prod.secret.exs # which should be versioned separately. -import_config "prod.secret.exs" +if File.exists?("./config/prod.secret.exs") do + import_config "prod.secret.exs" +else + "`config/prod.secret.exs` not found. You may want to create one by running `mix pleroma.instance gen`" + |> IO.warn([]) +end if File.exists?("./config/prod.exported_from_db.secret.exs"), do: import_config("prod.exported_from_db.secret.exs") diff --git a/config/test.exs b/config/test.exs index 87396a88d..d5c25f65e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config # We don't run a server during test. If one is required, # you can enable the server option below. @@ -133,6 +133,10 @@ ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, logger: Pleroma.LoggerMock +# Reduce recompilation time +# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects +config :phoenix, :plug_init_mode, :runtime + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 069421722..40e81cffb 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -8,9 +8,10 @@ For from source installations Pleroma configuration works by first importing the To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted. -## :chat +## :shout -* `enabled` - Enables the backend chat. Defaults to `true`. +* `enabled` - Enables the backend Shoutbox chat feature. Defaults to `true`. +* `limit` - Shout character limit. Defaults to `5_000` ## :instance * `name`: The instance’s name. @@ -19,7 +20,6 @@ To add configuration to your config file, you can copy it from the base config. * `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``. * `limit`: Posts character limit (CW/Subject included in the counter). * `description_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). * `avatar_upload_limit`: File size limit of user’s profile avatars. @@ -37,9 +37,9 @@ To add configuration to your config file, you can copy it from the base config. * `federating`: Enable federation with other instances. * `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. +* `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your 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. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details. -* `quarantined_instances`: List of ActivityPub instances where private (DMs, followers-only) activities will not be send. +* `quarantined_instances`: ActivityPub instances where private (DMs, followers-only) activities will not be send. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). * `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with older software for theses nicknames. @@ -135,15 +135,16 @@ To add configuration to your config file, you can copy it from the base config. Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `policies` under [:mrf](#mrf) section. #### :mrf_simple -* `media_removal`: List of instances to remove media from. -* `media_nsfw`: List of instances to put media as NSFW(sensitive) from. -* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline. -* `reject`: List of instances to reject any activities from. -* `accept`: List of instances to accept any activities from. -* `followers_only`: List of instances to decrease post visibility to only the followers, including for DM mentions. -* `report_removal`: List of instances to reject reports from. -* `avatar_removal`: List of instances to strip avatars from. -* `banner_removal`: List of instances to strip banners from. +* `media_removal`: List of instances to strip media attachments from and the reason for doing so. +* `media_nsfw`: List of instances to tag all media as NSFW (sensitive) from and the reason for doing so. +* `federated_timeline_removal`: List of instances to remove from the Federated Timeline (aka The Whole Known Network) and the reason for doing so. +* `reject`: List of instances to reject activities (except deletes) from and the reason for doing so. +* `accept`: List of instances to only accept activities (except deletes) from and the reason for doing so. +* `followers_only`: Force posts from the given instances to be visible by followers only and the reason for doing so. +* `report_removal`: List of instances to reject reports from and the reason for doing so. +* `avatar_removal`: List of instances to strip avatars from and the reason for doing so. +* `banner_removal`: List of instances to strip banners from and the reason for doing so. +* `reject_deletes`: List of instances to reject deletions from and the reason for doing so. #### :mrf_subchain This policy processes messages through an alternate pipeline when a given message matches certain criteria. @@ -229,6 +230,7 @@ Notes: ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances +* `blockers_visible`: Whether a user can see the posts of users who blocked them * `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question * `sign_object_fetches`: Sign object fetches with HTTP signatures * `authorized_fetch_mode`: Require HTTP signatures for AP fetches @@ -246,7 +248,7 @@ Notes: ### :frontend_configurations -This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options). +This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options). Frontends can access these settings at `/api/v1/pleroma/frontend_configurations` @@ -257,10 +259,7 @@ config :pleroma, :frontend_configurations, pleroma_fe: %{ theme: "pleroma-dark", # ... see /priv/static/static/config.json for the available keys. -}, - masto_fe: %{ - showInstanceSpecificPanel: true - } +} ``` These settings **need to be complete**, they will override the defaults. diff --git a/docs/configuration/howto_search_cjk.md b/docs/configuration/howto_search_cjk.md index d3ce28077..a73b10db3 100644 --- a/docs/configuration/howto_search_cjk.md +++ b/docs/configuration/howto_search_cjk.md @@ -5,7 +5,7 @@ Pleroma's full text search feature is powered by PostgreSQL's native [text searc ## Setup and test the new search config -In most cases, you would need an extension installed to support parsing CJK text. Here are a few extension you may choose from, or you are more than welcome to share additional ones you found working for you with the rest of Pleroma community. +In most cases, you would need an extension installed to support parsing CJK text. Here are a few extensions you may choose from, or you are more than welcome to share additional ones you found working for you with the rest of Pleroma community. * [a generic n-gram parser](https://github.com/huangjimmy/pg_cjk_parser) supports Simplifed/Traditional Chinese, Japanese, and Korean * [a Korean parser](https://github.com/i0seph/textsearch_ko) based on mecab @@ -34,7 +34,7 @@ Check output of the query, and see if it matches your expectation. mix pleroma.database set_text_search_config YOUR.CONFIG ``` -Note: index update may take a while. +Note: index update may take a while, and it can be done while the instance is up and running, so you may restart db connection as soon as you see `Recreate index` in task output. ## Restart database connection Since some changes above will only apply with a new database connection, you will have to restart either Pleroma or PostgreSQL process, or use `pg_terminate_backend` SQL command without restarting either. diff --git a/docs/configuration/mrf.md b/docs/configuration/mrf.md index 9e8c0a2d7..a31c26b9c 100644 --- a/docs/configuration/mrf.md +++ b/docs/configuration/mrf.md @@ -55,18 +55,18 @@ Servers should be configured as lists. ### Example -This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`: +This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`. We also give a reason why the moderation was done: ```elixir config :pleroma, :mrf, policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy] config :pleroma, :mrf_simple, - media_removal: ["illegalporn.biz"], - media_nsfw: ["porn.biz", "porn.business"], - reject: ["spam.com"], - federated_timeline_removal: ["spam.university"], - report_removal: ["whiny.whiner"] + media_removal: [{"illegalporn.biz", "Media can contain illegal contant"}], + media_nsfw: [{"porn.biz", "unmarked nsfw media"}, {"porn.business", "A lot of unmarked nsfw media"}], + reject: [{"spam.com", "They keep spamming our users"}], + federated_timeline_removal: [{"spam.university", "Annoying low-quality posts who otherwise fill up TWKN"}], + report_removal: [{"whiny.whiner", "Keep spamming us with irrelevant reports"}] ``` ### Use with Care @@ -82,7 +82,7 @@ For example, here is a sample policy module which rewrites all messages to "new ```elixir defmodule Pleroma.Web.ActivityPub.MRF.RewritePolicy do @moduledoc "MRF policy which rewrites all Notes to have 'new message content'." - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy # Catch messages which contain Note objects with actual data to filter. # Capture the object as `object`, the message content as `content` and the diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 8f855d251..f14081893 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -261,6 +261,46 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` +## `PATCH /api/v1/pleroma/admin/users/suggest` + +### Suggest a user + +Adds the user(s) to follower recommendations. + +- Params: + - `nicknames`: nicknames array +- Response: + +```json +{ + users: [ + { + // user object + } + ] +} +``` + +## `PATCH /api/v1/pleroma/admin/users/unsuggest` + +### Unsuggest a user + +Removes the user(s) from follower recommendations. + +- Params: + - `nicknames`: nicknames array +- Response: + +```json +{ + users: [ + { + // user object + } + ] +} +``` + ## `GET /api/v1/pleroma/admin/users/:nickname_or_id` ### Retrive the details of a user @@ -319,6 +359,22 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` +## `DELETE /api/v1/pleroma/admin/instances/:instance` + +### Delete all users and activities from a remote instance + +Note: this will trigger a job to remove instance content in the background. +It may take some time. + +- Params: + - `instance`: remote instance host +- Response: + - The `instance` name as a string + +```json +"lain.com" +``` + ## `GET /api/v1/pleroma/admin/statuses` ### Retrives all latest statuses diff --git a/docs/development/API/nodeinfo.md b/docs/development/API/nodeinfo.md new file mode 100644 index 000000000..0f998a1e6 --- /dev/null +++ b/docs/development/API/nodeinfo.md @@ -0,0 +1,347 @@ +# Nodeinfo + +See also [the Nodeinfo standard](https://nodeinfo.diaspora.software/). + +## `/.well-known/nodeinfo` +### The well-known path +* Method: `GET` +* Authentication: not required +* Params: none +* Response: JSON +* Example response: +```json +{ + "links":[ + { + "href":"https://example.com/nodeinfo/2.0.json", + "rel":"http://nodeinfo.diaspora.software/ns/schema/2.0" + }, + { + "href":"https://example.com/nodeinfo/2.1.json", + "rel":"http://nodeinfo.diaspora.software/ns/schema/2.1" + } + ] +} +``` + +## `/nodeinfo/2.0.json` +### Nodeinfo 2.0 +* Method: `GET` +* Authentication: not required +* Params: none +* Response: JSON +* Example response: +```json +{ + "metadata":{ + "accountActivationRequired":false, + "features":[ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "chat", + "shout", + "relay", + "pleroma_emoji_reactions", + "pleroma_chat_messages" + ], + "federation":{ + "enabled":true, + "exclusions":false, + "mrf_hashtag":{ + "federated_timeline_removal":[ + + ], + "reject":[ + + ], + "sensitive":[ + "nsfw" + ] + }, + "mrf_object_age":{ + "actions":[ + "delist", + "strip_followers" + ], + "threshold":604800 + }, + "mrf_policies":[ + "ObjectAgePolicy", + "TagPolicy", + "HashtagPolicy" + ], + "quarantined_instances":[ + + ] + }, + "fieldsLimits":{ + "maxFields":10, + "maxRemoteFields":20, + "nameLength":512, + "valueLength":2048 + }, + "invitesEnabled":false, + "mailerEnabled":false, + "nodeDescription":"Pleroma: An efficient and flexible fediverse server", + "nodeName":"Example", + "pollLimits":{ + "max_expiration":31536000, + "max_option_chars":200, + "max_options":20, + "min_expiration":0 + }, + "postFormats":[ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ], + "private":false, + "restrictedNicknames":[ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ], + "skipThreadContainment":true, + "staffAccounts":[ + "https://example.com/users/admin", + "https://example.com/users/staff" + ], + "suggestions":{ + "enabled":false + }, + "uploadLimits":{ + "avatar":2000000, + "background":4000000, + "banner":4000000, + "general":16000000 + } + }, + "openRegistrations":true, + "protocols":[ + "activitypub" + ], + "services":{ + "inbound":[ + + ], + "outbound":[ + + ] + }, + "software":{ + "name":"pleroma", + "version":"2.4.1" + }, + "usage":{ + "localPosts":27, + "users":{ + "activeHalfyear":129, + "activeMonth":70, + "total":235 + } + }, + "version":"2.0" +} +``` + +## `/nodeinfo/2.1.json` +### Nodeinfo 2.1 +* Method: `GET` +* Authentication: not required +* Params: none +* Response: JSON +* Example response: +```json +{ + "metadata":{ + "accountActivationRequired":false, + "features":[ + "pleroma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "chat", + "shout", + "relay", + "pleroma_emoji_reactions", + "pleroma_chat_messages" + ], + "federation":{ + "enabled":true, + "exclusions":false, + "mrf_hashtag":{ + "federated_timeline_removal":[ + + ], + "reject":[ + + ], + "sensitive":[ + "nsfw" + ] + }, + "mrf_object_age":{ + "actions":[ + "delist", + "strip_followers" + ], + "threshold":604800 + }, + "mrf_policies":[ + "ObjectAgePolicy", + "TagPolicy", + "HashtagPolicy" + ], + "quarantined_instances":[ + + ] + }, + "fieldsLimits":{ + "maxFields":10, + "maxRemoteFields":20, + "nameLength":512, + "valueLength":2048 + }, + "invitesEnabled":false, + "mailerEnabled":false, + "nodeDescription":"Pleroma: An efficient and flexible fediverse server", + "nodeName":"Example", + "pollLimits":{ + "max_expiration":31536000, + "max_option_chars":200, + "max_options":20, + "min_expiration":0 + }, + "postFormats":[ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode" + ], + "private":false, + "restrictedNicknames":[ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "check_password", + "dev", + "friend-requests", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "ostatus_subscribe", + "pleroma", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "user-search", + "user_exists", + "users", + "web", + "verify_credentials", + "update_credentials", + "relationships", + "search", + "confirmation_resend", + "mfa" + ], + "skipThreadContainment":true, + "staffAccounts":[ + "https://example.com/users/admin", + "https://example.com/users/staff" + ], + "suggestions":{ + "enabled":false + }, + "uploadLimits":{ + "avatar":2000000, + "background":4000000, + "banner":4000000, + "general":16000000 + } + }, + "openRegistrations":true, + "protocols":[ + "activitypub" + ], + "services":{ + "inbound":[ + + ], + "outbound":[ + + ] + }, + "software":{ + "name":"pleroma", + "repository":"https://git.pleroma.social/pleroma/pleroma", + "version":"2.4.1" + }, + "usage":{ + "localPosts":27, + "users":{ + "activeHalfyear":129, + "activeMonth":70, + "total":235 + } + }, + "version":"2.1" +} +``` + diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index d896f0ce7..0e7367a72 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -159,10 +159,12 @@ See [Admin-API](admin_api.md) "muting": false, "muting_notifications": false, "subscribing": true, + "notifying": true, "requested": false, "domain_blocking": false, "showing_reblogs": true, - "endorsed": false + "endorsed": false, + "note": "" } ``` @@ -183,10 +185,12 @@ See [Admin-API](admin_api.md) "muting": false, "muting_notifications": false, "subscribing": false, + "notifying": false, "requested": false, "domain_blocking": false, "showing_reblogs": true, - "endorsed": false + "endorsed": false, + "note": "" } ``` @@ -300,7 +304,7 @@ See [Admin-API](admin_api.md) * Note: Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`. -## `/api/v1/pleroma/notification_settings` +## `/api/pleroma/notification_settings` ### Updates user notification settings * Method `PUT` * Authentication: required diff --git a/docs/index.md b/docs/index.md index 80c5d2631..3799a0099 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,11 +16,4 @@ Installation instructions can be found in the installation section of these docs Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. ) and login with your username and password. (If you don't have an account yet, click on Register) ### Pleroma-FE -The default front-end used by Pleroma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](../frontend). - -### Mastodon interface -If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! -Just add a "/web" after your instance url (e.g. ) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! -The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation. - -Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. +The default front-end used by Pleroma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](../frontend). \ No newline at end of file diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 7eb1718f2..c37ff0c63 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -1,29 +1,14 @@ # Installing on Alpine Linux + +{! backend/installation/otp_vs_from_source_source.include !} + ## Installation This guide is a step-by-step installation guide for Alpine Linux. The instructions were verified against Alpine v3.10 standard image. You might miss additional dependencies if you use `netboot` instead. It assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -l -s $SHELL -c 'command'` instead. -### Required packages - -* `postgresql` -* `elixir` -* `erlang` -* `erlang-parsetools` -* `erlang-xmerl` -* `git` -* `file-dev` -* Development Tools -* `cmake` - -#### Optional packages used in this guide - -* `nginx` (preferred, example configs for other reverse proxies can be found in the repo) -* `certbot` (or any other ACME client for Let’s Encrypt certificates) -* `ImageMagick` -* `ffmpeg` -* `exiftool` +{! backend/installation/generic_dependencies.include !} ### Prepare the system @@ -117,7 +102,7 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` +* Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen` * Answer with `yes` if it asks you to install `rebar3`. * This may take some time, because parts of pleroma get compiled first. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. @@ -240,4 +225,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new -s $SHELL -c 'command'` instead. @@ -92,7 +95,7 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` +* Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen` * Answer with `yes` if it asks you to install `rebar3`. * This may take some time, because parts of pleroma get compiled first. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. @@ -215,4 +218,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new -s $SHELL -c 'command'` instead. +This guide will assume you are on Debian 11 (“bullseye”) or later. This guide should also work with Ubuntu 18.04 (“Bionic Beaver”) and later. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.digitalocean.com/community/tutorials/how-to-add-delete-and-grant-sudo-privileges-to-users-on-a-debian-vps). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -s $SHELL -c 'command'` instead. -### Required packages - -* `postgresql` (9.6+, Ubuntu 16.04 comes with 9.5, you can get a newer version from [here](https://www.postgresql.org/download/linux/ubuntu/)) -* `postgresql-contrib` (9.6+, same situtation as above) -* `elixir` (1.8+, Follow the guide to install from the Erlang Solutions repo or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user) -* `erlang-dev` -* `erlang-nox` -* `libmagic-dev` -* `git` -* `build-essential` -* `cmake` - -#### Optional packages used in this guide - -* `nginx` (preferred, example configs for other reverse proxies can be found in the repo) -* `certbot` (or any other ACME client for Let’s Encrypt certificates) -* `ImageMagick` -* `ffmpeg` -* `exiftool` +{! backend/installation/generic_dependencies.include !} ### Prepare the system @@ -40,20 +25,14 @@ sudo apt install git build-essential postgresql postgresql-contrib cmake libmagi ### Install Elixir and Erlang -* Download and add the Erlang repository: - -```shell -wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb -sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb -``` - -* Install Elixir and Erlang: +* Install Elixir and Erlang (you might need to use backports or [asdf](https://github.com/asdf-vm/asdf) on old systems): ```shell sudo apt update sudo apt install elixir erlang-dev erlang-nox ``` + ### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md) ```shell @@ -90,7 +69,7 @@ cd /opt/pleroma sudo -Hu pleroma mix deps.get ``` -* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` +* Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen` * Answer with `yes` if it asks you to install `rebar3`. * This may take some time, because parts of pleroma get compiled first. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. @@ -202,4 +181,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new > /etc/apk/repositories + awk 'NR==2' /etc/apk/repositories | sed 's/main/community/' | tee -a /etc/apk/repositories apk update apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot file-dev ``` @@ -50,7 +54,6 @@ Per [`docs/installation/optional/media_graphics_packages.md`](optional/media_gra === "Alpine" ``` - echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update apk add imagemagick ffmpeg exiftool ``` @@ -232,7 +235,7 @@ At this point if you open your (sub)domain in a browser you should see a 502 err If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors. -Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://irc.pleroma.social) or via matrix at , you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new) +Questions about the installation or didn’t it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new). ## Post installation @@ -301,4 +304,4 @@ This will create an account withe the username of 'joeuser' with the email addre ## Questions -Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. +Questions about the installation or didn’t it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new). diff --git a/docs/installation/otp_vs_from_source.include b/docs/installation/otp_vs_from_source.include new file mode 100644 index 000000000..63e837a53 --- /dev/null +++ b/docs/installation/otp_vs_from_source.include @@ -0,0 +1,3 @@ +## OTP releases vs from-source installations + +There are two ways to install Pleroma. You can use OTP releases or do a from-source installation. OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it, it is easily administered via the provided shell script to open up a remote console, start/stop/restart the release, start in the background, send remote commands, and more. With from source installations you install Pleroma from source, meaning you have to install certain dependencies like Erlang+Elixir and compile Pleroma yourself. diff --git a/docs/installation/otp_vs_from_source_source.include b/docs/installation/otp_vs_from_source_source.include new file mode 100644 index 000000000..63482b69d --- /dev/null +++ b/docs/installation/otp_vs_from_source_source.include @@ -0,0 +1,3 @@ +{! backend/installation/otp_vs_from_source.include !} + +This guide covers a from-source installation. To install using OTP releases, please check out [the OTP guide](./otp_en.md). diff --git a/docs/installation/yunohost_en.md b/docs/installation/yunohost_en.md new file mode 100644 index 000000000..4c34e85d5 --- /dev/null +++ b/docs/installation/yunohost_en.md @@ -0,0 +1,9 @@ +# Installing on Yunohost + +[YunoHost](https://yunohost.org) is a server operating system aimed at self-hosting. The YunoHost community maintains a package of Pleroma which allows you to install Pleroma on YunoHost. You can install it via the normal way through the admin web interface, or through the CLI. More information can be found at [the repo of the package](https://github.com/YunoHost-Apps/pleroma_ynh). + +## Questions + +Questions and problems related to the YunoHost parts can be done through the [regular YunoHost channels](https://yunohost.org/en/help). + +For questions about Pleroma, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC. diff --git a/installation/download-mastofe-build.sh b/installation/download-mastofe-build.sh deleted file mode 100755 index ee353c48c..000000000 --- a/installation/download-mastofe-build.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only -project_id="74" -project_branch="rebase/glitch-soc" -static_dir="instance/static" -# For bundling: -# project_branch="pleroma" -# static_dir="priv/static" - -if [ ! -d "${static_dir}" ] -then - echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleroma’s repository?" - exit 1 -fi - -last_modified="$(curl --fail -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)" - -echo "branch:${project_branch}" -echo "Last-Modified:${last_modified}" - -artifact="mastofe.zip" - -if [ "${last_modified}x" = "x" ] -then - echo "ERROR: Couldn't get the modification date of the latest build archive, maybe it expired, exiting..." - exit 1 -fi - -if [ -e mastofe.timestamp ] && [ "$(cat mastofe.timestamp)" = "${last_modified}" ] -then - echo "MastoFE is up-to-date, exiting..." - exit 0 -fi - -curl --fail -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit - -# TODO: Update the emoji as well -rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit -unzip -q "${artifact}" || exit - -cp public/assets/sw.js "${static_dir}/sw.js" || exit -cp -r public/packs "${static_dir}/packs" || exit - -echo "${last_modified}" > mastofe.timestamp -rm -fr public -rm -i "${artifact}" diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 22502a522..05ff8076f 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -286,9 +286,7 @@ defp migrate_from_db(opts) do file = File.open!(tmp_config_path) shell_info( - "Saving database configuration settings to #{tmp_config_path}. Copy it to the #{ - Path.dirname(config_path) - } manually." + "Saving database configuration settings to #{tmp_config_path}. Copy it to the #{Path.dirname(config_path)} manually." ) write_config(file, tmp_config_path, opts) diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index e7f4b67a4..a973beaa9 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -96,6 +96,15 @@ def run(["prune_objects" | args]) do ) |> Repo.delete_all(timeout: :infinity) + prune_hashtags_query = """ + DELETE FROM hashtags AS ht + WHERE NOT EXISTS ( + SELECT 1 FROM hashtags_objects hto + WHERE ht.id = hto.hashtag_id) + """ + + Repo.query(prune_hashtags_query) + if Keyword.get(options, :vacuum) do Maintenance.vacuum("full") end @@ -200,7 +209,9 @@ def run(["set_text_search_config", tsconfig]) do new.fts_content := to_tsvector(new.data->>'content'); RETURN new; END - $$ LANGUAGE plpgsql" + $$ LANGUAGE plpgsql", + [], + timeout: :infinity ) shell_info("Refresh RUM index") @@ -210,7 +221,9 @@ def run(["set_text_search_config", tsconfig]) do Ecto.Adapters.SQL.query!( Pleroma.Repo, - "CREATE INDEX objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); " + "CREATE INDEX CONCURRENTLY objects_fts ON objects USING gin(to_tsvector('#{tsconfig}', data->>'content')); ", + [], + timeout: :infinity ) end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index da27a99d0..d98cb8e37 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -199,6 +199,7 @@ def run(["gen" | rest]) do secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) + lv_signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1) template_dir = Application.app_dir(:pleroma, "priv") <> "/templates" @@ -217,6 +218,7 @@ def run(["gen" | rest]) do secret: secret, jwt_secret: jwt_secret, signing_salt: signing_salt, + lv_signing_salt: lv_signing_salt, web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), web_push_private_key: Base.url_encode64(web_push_private_key, padding: false), db_configurable?: db_configurable?, diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 53d5fc6d9..e848222b8 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -51,9 +51,7 @@ def run(["new", nickname, email | rest]) do A user will be created with the following information: - nickname: #{nickname} - email: #{email} - - password: #{ - if(generated_password?, do: "[generated; a reset link will be created]", else: password) - } + - password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)} - name: #{name} - bio: #{bio} - moderator: #{if(moderator?, do: "true", else: "false")} @@ -114,15 +112,9 @@ def run(["reset_password", nickname]) do {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do shell_info("Generated password reset token for #{user.nickname}") - IO.puts( - "URL: #{ - Pleroma.Web.Router.Helpers.reset_password_url( - Pleroma.Web.Endpoint, - :reset, - token.token - ) - }" - ) + IO.puts("URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, + :reset, + token.token)}") else _ -> shell_error("No local user #{nickname}") @@ -321,9 +313,7 @@ def run(["invites"]) do end shell_info( - "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{ - invite.used - }#{expire_info}#{using_info}" + "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}" ) end) end @@ -424,9 +414,7 @@ def run(["list"]) do users |> Enum.each(fn user -> shell_info( - "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{ - user.is_locked - }, is_active: #{user.is_active}" + "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}" ) end) end) diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 53beca5e6..4106feef6 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -292,7 +292,8 @@ def get_in_reply_to_activity(%Activity{} = activity) do get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false)) end - def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) + def normalize(%Activity{data: %{"id" => ap_id}}), do: get_by_ap_id_with_object(ap_id) + def normalize(%{"id" => ap_id}), do: get_by_ap_id_with_object(ap_id) def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id) def normalize(_), do: nil @@ -301,7 +302,7 @@ def delete_all_by_object_ap_id(id) when is_binary(id) do |> Queries.by_object_id() |> Queries.exclude_type("Delete") |> select([u], u) - |> Repo.delete_all() + |> Repo.delete_all(timeout: :infinity) |> elem(1) |> Enum.find(fn %{data: %{"type" => "Create", "object" => ap_id}} when is_binary(ap_id) -> ap_id == id @@ -313,13 +314,15 @@ def delete_all_by_object_ap_id(id) when is_binary(id) do def delete_all_by_object_ap_id(_), do: nil - defp purge_web_resp_cache(%Activity{} = activity) do - %{path: path} = URI.parse(activity.data["id"]) - @cachex.del(:web_resp_cache, path) + defp purge_web_resp_cache(%Activity{data: %{"id" => id}} = activity) when is_binary(id) do + with %{path: path} <- URI.parse(id) do + @cachex.del(:web_resp_cache, path) + end + activity end - defp purge_web_resp_cache(nil), do: nil + defp purge_web_resp_cache(activity), do: activity def follow_accepted?( %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity @@ -359,11 +362,9 @@ def following_requests_for_actor(%User{ap_id: ap_id}) do end def restrict_deactivated_users(query) do - deactivated_users = - from(u in User.Query.build(%{deactivated: true}), select: u.ap_id) - |> Repo.all() + deactivated_users_query = from(u in User.Query.build(%{deactivated: true}), select: u.ap_id) - Activity.Queries.exclude_authors(query, deactivated_users) + from(activity in query, where: activity.actor not in subquery(deactivated_users_query)) end defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex new file mode 100644 index 000000000..0bf393836 --- /dev/null +++ b/lib/pleroma/activity/html.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Activity.HTML do + alias Pleroma.HTML + alias Pleroma.Object + + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + def get_cached_scrubbed_html_for_activity( + content, + scrubbers, + activity, + key \\ "", + callback \\ fn x -> x end + ) do + key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" + + @cachex.fetch!(:scrubber_cache, key, fn _key -> + object = Object.normalize(activity, fetch: false) + HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback) + end) + end + + def get_cached_stripped_html_for_activity(content, activity, key) do + get_cached_scrubbed_html_for_activity( + content, + FastSanitize.Sanitizer.StripTags, + activity, + key, + &HtmlEntities.decode/1 + ) + end + + defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do + generate_scrubber_signature([scrubber]) + end + + defp generate_scrubber_signature(scrubbers) do + Enum.reduce(scrubbers, "", fn scrubber, signature -> + "#{signature}#{to_string(scrubber)}" + end) + end +end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index ed898ba4f..09671f621 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -26,19 +26,23 @@ def search(user, search_query, options \\ []) do :plain end - Activity - |> Activity.with_preloaded_object() - |> Activity.restrict_deactivated_users() - |> restrict_public() - |> query_with(index_type, search_query, search_function) - |> maybe_restrict_local(user) - |> maybe_restrict_author(author) - |> maybe_restrict_blocked(user) - |> Pagination.fetch_paginated( - %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum}, - :offset - ) - |> maybe_fetch(user, search_query) + try do + Activity + |> Activity.with_preloaded_object() + |> Activity.restrict_deactivated_users() + |> restrict_public() + |> query_with(index_type, search_query, search_function) + |> maybe_restrict_local(user) + |> maybe_restrict_author(author) + |> maybe_restrict_blocked(user) + |> Pagination.fetch_paginated( + %{"offset" => offset, "limit" => limit, "skip_order" => index_type == :rum}, + :offset + ) + |> maybe_fetch(user, search_query) + rescue + _ -> maybe_fetch([], user, search_query) + end end def maybe_restrict_author(query, %User{} = author) do @@ -61,10 +65,17 @@ defp restrict_public(q) do end defp query_with(q, :gin, search_query, :plain) do + %{rows: [[tsc]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select current_setting('default_text_search_config')::regconfig::oid;" + ) + from([a, o] in q, where: fragment( - "to_tsvector(?->>'content') @@ plainto_tsquery(?)", + "to_tsvector(?::oid::regconfig, ?->>'content') @@ plainto_tsquery(?)", + ^tsc, o.data, ^search_query ) @@ -72,10 +83,17 @@ defp query_with(q, :gin, search_query, :plain) do end defp query_with(q, :gin, search_query, :websearch) do + %{rows: [[tsc]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select current_setting('default_text_search_config')::regconfig::oid;" + ) + from([a, o] in q, where: fragment( - "to_tsvector(?->>'content') @@ websearch_to_tsquery(?)", + "to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)", + ^tsc, o.data, ^search_query ) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 06d399b2e..9824e0a4a 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -25,7 +25,7 @@ def user_agent do if Process.whereis(Pleroma.Web.Endpoint) do case Config.get([:http, :user_agent], :default) do :default -> - info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>" + info = "#{Pleroma.Web.Endpoint.url()} <#{Config.get([:instance, :email], "")}>" named_version() <> "; " <> info custom -> @@ -102,7 +102,7 @@ def start(_type, _args) do ] ++ task_children(@mix_env) ++ dont_run_in_test(@mix_env) ++ - chat_child(chat_enabled?()) ++ + shout_child(shout_enabled?()) ++ [Pleroma.Gopher.Server] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html @@ -216,7 +216,7 @@ def build_cachex(type, opts), type: :worker } - defp chat_enabled?, do: Config.get([:chat, :enabled]) + defp shout_enabled?, do: Config.get([:shout, :enabled]) defp dont_run_in_test(env) when env in [:test, :benchmark], do: [] @@ -237,14 +237,14 @@ defp background_migrators do ] end - defp chat_child(true) do + defp shout_child(true) do [ - Pleroma.Web.ChatChannel.ChatChannelState, + Pleroma.Web.ShoutChannel.ShoutChannelState, {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]} ] end - defp chat_child(_), do: [] + defp shout_child(_), do: [] defp task_children(:test) do [ diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index c412dec5e..a56311a65 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -164,9 +164,12 @@ defp do_check_rum!(setting, migrate) do defp check_system_commands!(:ok) do filter_commands_statuses = [ - check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"), - check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"), - check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify") + check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"), + check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"), + check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"), + check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"), + check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "convert"), + check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "ffprobe") ] preview_proxy_commands_status = diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 4a2e255f7..a38faa5b8 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -19,9 +19,7 @@ def on_shell(username, _pubkey, _ip, _port) do def on_connect(username, ip, port, method) do Logger.debug(fn -> """ - Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{ - inspect(port) - } using #{inspect(method)} + Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{inspect(port)} using #{inspect(method)} """ end) end diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 24aa5993b..029ee8b65 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -20,6 +20,140 @@ defmodule Pleroma.Config.DeprecationWarnings do "\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"} ] + def check_simple_policy_tuples do + has_strings = + Config.get([:mrf_simple]) + |> Enum.any?(fn {_, v} -> Enum.any?(v, &is_binary/1) end) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the SimplePolicy configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf_simple, + media_removal: ["instance.tld"], + media_nsfw: ["instance.tld"], + federated_timeline_removal: ["instance.tld"], + report_removal: ["instance.tld"], + reject: ["instance.tld"], + followers_only: ["instance.tld"], + accept: ["instance.tld"], + avatar_removal: ["instance.tld"], + banner_removal: ["instance.tld"], + reject_deletes: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf_simple, + media_removal: [{"instance.tld", "Reason for media removal"}], + media_nsfw: [{"instance.tld", "Reason for media nsfw"}], + federated_timeline_removal: [{"instance.tld", "Reason for federated timeline removal"}], + report_removal: [{"instance.tld", "Reason for report removal"}], + reject: [{"instance.tld", "Reason for reject"}], + followers_only: [{"instance.tld", "Reason for followers only"}], + accept: [{"instance.tld", "Reason for accept"}], + avatar_removal: [{"instance.tld", "Reason for avatar removal"}], + banner_removal: [{"instance.tld", "Reason for banner removal"}], + reject_deletes: [{"instance.tld", "Reason for reject deletes"}] + ``` + """) + + new_config = + Config.get([:mrf_simple]) + |> Enum.map(fn {k, v} -> + {k, + Enum.map(v, fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end)} + end) + + Config.put([:mrf_simple], new_config) + + :error + else + :ok + end + end + + def check_quarantined_instances_tuples do + has_strings = Config.get([:instance, :quarantined_instances]) |> Enum.any?(&is_binary/1) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the quarantined_instances configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :instance, + quarantined_instances: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :instance, + quarantined_instances: [{"instance.tld", "Reason for quarantine"}] + ``` + """) + + new_config = + Config.get([:instance, :quarantined_instances]) + |> Enum.map(fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end) + + Config.put([:instance, :quarantined_instances], new_config) + + :error + else + :ok + end + end + + def check_transparency_exclusions_tuples do + has_strings = Config.get([:mrf, :transparency_exclusions]) |> Enum.any?(&is_binary/1) + + if has_strings do + Logger.warn(""" + !!!DEPRECATION WARNING!!! + Your config is using strings in the transparency_exclusions configuration instead of tuples. They should work for now, but you are advised to change to the new configuration to prevent possible issues later: + + ``` + config :pleroma, :mrf, + transparency_exclusions: ["instance.tld"] + ``` + + Is now + + + ``` + config :pleroma, :mrf, + transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}] + ``` + """) + + new_config = + Config.get([:mrf, :transparency_exclusions]) + |> Enum.map(fn + {instance, reason} -> {instance, reason} + instance -> {instance, ""} + end) + + Config.put([:mrf, :transparency_exclusions], new_config) + + :error + else + :ok + end + end + def check_hellthread_threshold do if Config.get([:mrf_hellthread, :threshold]) do Logger.warn(""" @@ -34,19 +168,24 @@ def check_hellthread_threshold do end def warn do - with :ok <- check_hellthread_threshold(), - :ok <- check_old_mrf_config(), - :ok <- check_media_proxy_whitelist_config(), - :ok <- check_welcome_message_config(), - :ok <- check_gun_pool_options(), - :ok <- check_activity_expiration_config(), - :ok <- check_remote_ip_plug_name(), - :ok <- check_uploders_s3_public_endpoint() do - :ok - else - _ -> - :error - end + [ + check_hellthread_threshold(), + check_old_mrf_config(), + check_media_proxy_whitelist_config(), + check_welcome_message_config(), + check_gun_pool_options(), + check_activity_expiration_config(), + check_remote_ip_plug_name(), + check_uploders_s3_public_endpoint(), + check_old_chat_shoutbox(), + check_quarantined_instances_tuples(), + check_transparency_exclusions_tuples(), + check_simple_policy_tuples() + ] + |> Enum.reduce(:ok, fn + :ok, :ok -> :ok + _, _ -> :error + end) end def check_welcome_message_config do @@ -215,4 +354,27 @@ def check_uploders_s3_public_endpoint do :ok end end + + @spec check_old_chat_shoutbox() :: :ok | nil + def check_old_chat_shoutbox do + instance_config = Pleroma.Config.get([:instance]) + chat_config = Pleroma.Config.get([:chat]) || [] + + use_old_config = + Keyword.has_key?(instance_config, :chat_limit) or + Keyword.has_key?(chat_config, :enabled) + + if use_old_config do + Logger.error(""" + !!!DEPRECATION WARNING!!! + Your config is using the old namespace for the Shoutbox configuration. You need to convert to the new namespace. e.g., + \n* `config :pleroma, :chat, enabled` and `config :pleroma, :instance, chat_limit` are now equal to: + \n* `config :pleroma, :shout, enabled` and `config :pleroma, :shout, limit` + """) + + :error + else + :ok + end + end end diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index b64d06707..2a945999e 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -3,9 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.Loader do + # These modules are only being used as keys here (for equality check), + # so it's okay to use `Module.concat/1` to have the compiler ignore them. @reject_keys [ - Pleroma.Repo, - Pleroma.Web.Endpoint, + Module.concat(["Pleroma.Repo"]), + Module.concat(["Pleroma.Web.Endpoint"]), :env, :configurable_from_database, :database, diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex index 3e63bca40..53ea7d7be 100644 --- a/lib/pleroma/config/oban.ex +++ b/lib/pleroma/config/oban.ex @@ -21,9 +21,7 @@ def warn do """ !!!OBAN CONFIG WARNING!!! You are using old workers in Oban crontab settings, which were removed. - Please, remove setting from crontab in your config file (prod.secret.exs): #{ - inspect(setting) - } + Please, remove setting from crontab in your config file (prod.secret.exs): #{inspect(setting)} """ |> Logger.warn() diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index aad45aab8..5371aae7a 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -13,23 +13,25 @@ defmodule Pleroma.Config.TransferTask do @type env() :: :test | :benchmark | :dev | :prod - @reboot_time_keys [ - {:pleroma, :hackney_pools}, - {:pleroma, :chat}, - {:pleroma, Oban}, - {:pleroma, :rate_limit}, - {:pleroma, :markup}, - {:pleroma, :streamer}, - {:pleroma, :pools}, - {:pleroma, :connections_pool} - ] + defp reboot_time_keys, + do: [ + {:pleroma, :hackney_pools}, + {:pleroma, :shout}, + {:pleroma, Oban}, + {:pleroma, :rate_limit}, + {:pleroma, :markup}, + {:pleroma, :streamer}, + {:pleroma, :pools}, + {:pleroma, :connections_pool} + ] - @reboot_time_subkeys [ - {:pleroma, Pleroma.Captcha, [:seconds_valid]}, - {:pleroma, Pleroma.Upload, [:proxy_remote]}, - {:pleroma, :instance, [:upload_limit]}, - {:pleroma, :gopher, [:enabled]} - ] + defp reboot_time_subkeys, + do: [ + {:pleroma, Pleroma.Captcha, [:seconds_valid]}, + {:pleroma, Pleroma.Upload, [:proxy_remote]}, + {:pleroma, :instance, [:upload_limit]}, + {:pleroma, :gopher, [:enabled]} + ] def start_link(restart_pleroma? \\ true) do load_and_update_env([], restart_pleroma?) @@ -146,9 +148,7 @@ defp update({group, key, value, merged}) do rescue error -> error_msg = - "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{ - inspect(value) - } error: #{inspect(error)}" + "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{inspect(value)} error: #{inspect(error)}" Logger.warn(error_msg) @@ -165,12 +165,12 @@ def pleroma_need_restart?(group, key, value) do end defp group_and_key_need_reboot?(group, key) do - Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end) + Enum.any?(reboot_time_keys(), fn {g, k} -> g == group and k == key end) end defp group_and_subkey_need_reboot?(group, key, value) do Keyword.keyword?(value) and - Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} -> + Enum.any?(reboot_time_subkeys(), fn {g, k, subkeys} -> g == group and k == key and Enum.any?(Keyword.keys(value), &(&1 in subkeys)) end) diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index b24338cc6..bf92f65cb 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -27,6 +27,4 @@ defmodule Pleroma.Constants do do: ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ) - - def as_local_public, do: Pleroma.Web.base_url() <> "/#Public" end diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index 2a9addabc..0e3e1e5de 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -9,7 +9,8 @@ mute: 2, reblog_mute: 3, notification_mute: 4, - inverse_subscription: 5 + inverse_subscription: 5, + suggestion_dismiss: 6 ) defenum(Pleroma.FollowingRelationship.State, diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex index af4b0e527..06fed8fb3 100644 --- a/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex +++ b/lib/pleroma/ecto_type/activity_pub/object_validators/recipients.ex @@ -13,21 +13,33 @@ def cast(object) when is_binary(object) do cast([object]) end - def cast(data) when is_list(data) do - data - |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} -> - case ObjectID.cast(element) do - {:ok, id} -> - {:cont, {:ok, [id | list]}} - - _ -> - {:halt, :error} - end - end) + def cast(object) when is_map(object) do + case ObjectID.cast(object) do + {:ok, data} -> {:ok, [data]} + _ -> :error + end end - def cast(_) do - :error + def cast(data) when is_list(data) do + data = + data + |> Enum.reduce_while([], fn element, list -> + case ObjectID.cast(element) do + {:ok, id} -> + {:cont, [id | list]} + + _ -> + {:cont, list} + end + end) + |> Enum.sort() + |> Enum.uniq() + + {:ok, data} + end + + def cast(data) do + {:error, data} end def dump(data) do diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index 5fe74e2f7..88bc78aec 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -73,7 +73,7 @@ def report(to, reporter, account, statuses, comment) do #{comment_html} #{statuses_html}

- View Reports in AdminFE + View Reports in AdminFE """ new() @@ -87,7 +87,7 @@ def new_unapproved_registration(to, account) do html_body = """

New account for review: @#{account.nickname}

#{HTML.strip_tags(account.registration_reason)}
- Visit AdminFE + Visit AdminFE """ new() diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 52f3d419d..e38c681ba 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -5,15 +5,22 @@ defmodule Pleroma.Emails.UserEmail do @moduledoc "User emails" - use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email} - alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.Endpoint alias Pleroma.Web.Router + import Swoosh.Email + import Phoenix.Swoosh, except: [render_body: 3] import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0] + def render_body(email, template, assigns \\ %{}) do + email + |> put_new_layout({Pleroma.Web.LayoutView, :email}) + |> put_new_view(Pleroma.Web.EmailView) + |> Phoenix.Swoosh.render_body(template, assigns) + end + defp recipient(email, nil), do: email defp recipient(email, name), do: {name, email} defp recipient(%User{} = user), do: recipient(user.email, user.name) diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex index 50150e951..191451952 100644 --- a/lib/pleroma/emoji/formatter.ex +++ b/lib/pleroma/emoji/formatter.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Emoji.Formatter do alias Pleroma.Emoji alias Pleroma.HTML - alias Pleroma.Web + alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy def emojify(text) do @@ -44,7 +44,7 @@ def get_emoji_map(text) when is_binary(text) do Emoji.get_all() |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end) |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc -> - Map.put(acc, name, to_string(URI.merge(Web.base_url(), file))) + Map.put(acc, name, to_string(URI.merge(Endpoint.url(), file))) end) end diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex index 67acd7069..abc95d902 100644 --- a/lib/pleroma/emoji/loader.ex +++ b/lib/pleroma/emoji/loader.ex @@ -60,9 +60,7 @@ def load do if not Enum.empty?(files) do Logger.warn( - "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{ - Enum.join(files, ", ") - }" + "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{Enum.join(files, ", ")}" ) end @@ -105,6 +103,7 @@ defp load_pack(pack_dir, emoji_groups) do pack_file = Path.join(pack_dir, "pack.json") if File.exists?(pack_file) do + Logger.info("Loading emoji pack from JSON: #{pack_file}") contents = Jason.decode!(File.read!(pack_file)) contents["files"] @@ -117,14 +116,13 @@ defp load_pack(pack_dir, emoji_groups) do emoji_txt = Path.join(pack_dir, "emoji.txt") if File.exists?(emoji_txt) do + Logger.info("Loading emoji pack from emoji.txt: #{emoji_txt}") load_from_file(emoji_txt, emoji_groups) else extensions = Config.get([:emoji, :pack_extensions]) Logger.info( - "No emoji.txt found for pack \"#{pack_name}\", assuming all #{ - Enum.join(extensions, ", ") - } files are emoji" + "No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji" ) make_shortcode_to_file_map(pack_dir, extensions) diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 764e347ec..ae37946ab 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -62,7 +62,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do tag = String.downcase(tag) - url = "#{Pleroma.Web.base_url()}/tag/#{tag}" + url = "#{Pleroma.Web.Endpoint.url()}/tag/#{tag}" link = Phoenix.HTML.Tag.content_tag(:a, tag_text, diff --git a/lib/pleroma/gun.ex b/lib/pleroma/gun.ex index f9c828fac..bef1c9872 100644 --- a/lib/pleroma/gun.ex +++ b/lib/pleroma/gun.ex @@ -11,9 +11,7 @@ defmodule Pleroma.Gun do @callback await(pid(), reference()) :: {:response, :fin, 200, []} @callback set_owner(pid(), pid()) :: :ok - @api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) - - defp api, do: @api + defp api, do: Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) def open(host, port, opts), do: api().open(host, port, opts) diff --git a/lib/pleroma/gun/conn.ex b/lib/pleroma/gun/conn.ex index a56625699..a1210eabf 100644 --- a/lib/pleroma/gun/conn.ex +++ b/lib/pleroma/gun/conn.ex @@ -57,9 +57,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do else error -> Logger.warn( - "Opening proxied connection to #{compose_uri_log(uri)} failed with error #{ - inspect(error) - }" + "Opening proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}" ) error @@ -93,9 +91,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do else error -> Logger.warn( - "Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{ - inspect(error) - }" + "Opening socks proxied connection to #{compose_uri_log(uri)} failed with error #{inspect(error)}" ) error diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex index c37b62bf2..4c643d7cb 100644 --- a/lib/pleroma/gun/connection_pool/reclaimer.ex +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -5,11 +5,11 @@ defmodule Pleroma.Gun.ConnectionPool.Reclaimer do use GenServer, restart: :temporary - @registry Pleroma.Gun.ConnectionPool + defp registry, do: Pleroma.Gun.ConnectionPool def start_monitor do pid = - case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do + case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do {:ok, pid} -> pid @@ -46,7 +46,7 @@ def handle_continue(:reclaim, _) do # {worker_pid, crf, last_reference} end) unused_conns = Registry.select( - @registry, + registry(), [ {{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]} ] diff --git a/lib/pleroma/gun/connection_pool/worker.ex b/lib/pleroma/gun/connection_pool/worker.ex index 02bfff274..a3fa75386 100644 --- a/lib/pleroma/gun/connection_pool/worker.ex +++ b/lib/pleroma/gun/connection_pool/worker.ex @@ -6,10 +6,10 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do alias Pleroma.Gun use GenServer, restart: :temporary - @registry Pleroma.Gun.ConnectionPool + defp registry, do: Pleroma.Gun.ConnectionPool def start_link([key | _] = opts) do - GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}}) + GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {registry(), key}}) end @impl true @@ -24,7 +24,7 @@ def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do time = :erlang.monotonic_time(:millisecond) {_, _} = - Registry.update_value(@registry, key, fn _ -> + Registry.update_value(registry(), key, fn _ -> {conn_pid, [client_pid], 1, time} end) @@ -65,7 +65,7 @@ def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} = time = :erlang.monotonic_time(:millisecond) {{conn_pid, used_by, _, _}, _} = - Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> + 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) @@ -92,7 +92,7 @@ def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} = @impl true def handle_call(: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} -> + 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) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 2dfdca693..bee66169d 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -49,31 +49,6 @@ def filter_tags(html, scrubber) do def filter_tags(html), do: filter_tags(html, nil) def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags) - def get_cached_scrubbed_html_for_activity( - content, - scrubbers, - activity, - key \\ "", - callback \\ fn x -> x end - ) do - key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" - - @cachex.fetch!(:scrubber_cache, key, fn _key -> - object = Pleroma.Object.normalize(activity, fetch: false) - ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback) - end) - end - - def get_cached_stripped_html_for_activity(content, activity, key) do - get_cached_scrubbed_html_for_activity( - content, - FastSanitize.Sanitizer.StripTags, - activity, - key, - &HtmlEntities.decode/1 - ) - end - def ensure_scrubbed_html( content, scrubbers, @@ -92,16 +67,6 @@ def ensure_scrubbed_html( end end - defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do - generate_scrubber_signature([scrubber]) - end - - defp generate_scrubber_signature(scrubbers) do - Enum.reduce(scrubbers, "", fn scrubber, signature -> - "#{signature}#{to_string(scrubber)}" - end) - end - def extract_first_external_url_from_object(%{data: %{"content" => content}} = object) when is_binary(content) do unless object.data["fake"] do diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex index 82c7fd654..251539f34 100644 --- a/lib/pleroma/http/adapter_helper/gun.ex +++ b/lib/pleroma/http/adapter_helper/gun.ex @@ -54,8 +54,8 @@ def pool_timeout(pool) do Config.get([:pools, pool, :recv_timeout], default) end - @prefix Pleroma.Gun.ConnectionPool def limiter_setup do + prefix = Pleroma.Gun.ConnectionPool wait = Config.get([:connections_pool, :connection_acquisition_wait]) retries = Config.get([:connections_pool, :connection_acquisition_retries]) @@ -66,7 +66,7 @@ def limiter_setup do max_waiting = Keyword.get(opts, :max_waiting, 10) result = - ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting, + ConcurrentLimiter.new(:"#{prefix}.#{name}", max_running, max_waiting, wait: wait, max_retries: retries ) diff --git a/lib/pleroma/http/web_push.ex b/lib/pleroma/http/web_push.ex index 51f72fbf8..16bbe6e8c 100644 --- a/lib/pleroma/http/web_push.ex +++ b/lib/pleroma/http/web_push.ex @@ -5,8 +5,8 @@ defmodule Pleroma.HTTP.WebPush do @moduledoc false - def post(url, payload, headers) do + def post(url, payload, headers, options \\ []) do list_headers = Map.to_list(headers) - Pleroma.HTTP.post(url, payload, list_headers) + Pleroma.HTTP.post(url, payload, list_headers, options) end end diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 80addcc52..6b57e56da 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -5,13 +5,18 @@ defmodule Pleroma.Instances do @moduledoc "Instances context." - @adapter Pleroma.Instances.Instance + alias Pleroma.Instances.Instance - defdelegate filter_reachable(urls_or_hosts), to: @adapter - defdelegate reachable?(url_or_host), to: @adapter - defdelegate set_reachable(url_or_host), to: @adapter - defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: @adapter - defdelegate get_consistently_unreachable(), to: @adapter + def filter_reachable(urls_or_hosts), do: Instance.filter_reachable(urls_or_hosts) + + def reachable?(url_or_host), do: Instance.reachable?(url_or_host) + + def set_reachable(url_or_host), do: Instance.set_reachable(url_or_host) + + def set_unreachable(url_or_host, unreachable_since \\ nil), + do: Instance.set_unreachable(url_or_host, unreachable_since) + + def get_consistently_unreachable, do: Instance.get_consistently_unreachable() def set_consistently_unreachable(url_or_host), do: set_unreachable(url_or_host, reachability_datetime_threshold()) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 4d0e8034d..2f338b3e2 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -8,6 +8,8 @@ defmodule Pleroma.Instances.Instance do alias Pleroma.Instances alias Pleroma.Instances.Instance alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Workers.BackgroundWorker use Ecto.Schema @@ -195,4 +197,24 @@ defp scrape_favicon(%URI{} = instance_uri) do nil end end + + @doc """ + Deletes all users from an instance in a background task, thus also deleting + all of those users' activities and notifications. + """ + def delete_users_and_activities(host) when is_binary(host) do + BackgroundWorker.enqueue("delete_instance", %{"host" => host}) + end + + def perform(:delete_instance, host) when is_binary(host) do + User.Query.build(%{nickname: "@#{host}"}) + |> Repo.chunk_stream(100, :batches) + |> Stream.each(fn users -> + users + |> Enum.each(fn user -> + User.perform(:delete, user) + end) + end) + |> Stream.run() + end end diff --git a/lib/pleroma/maintenance.ex b/lib/pleroma/maintenance.ex index f1058b68a..41c799712 100644 --- a/lib/pleroma/maintenance.ex +++ b/lib/pleroma/maintenance.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Maintenance do def vacuum(args) do case args do "analyze" -> - Logger.info("Runnning VACUUM ANALYZE.") + Logger.info("Running VACUUM ANALYZE.") Repo.query!( "vacuum analyze;", @@ -18,7 +18,7 @@ def vacuum(args) do ) "full" -> - Logger.info("Runnning VACUUM FULL.") + Logger.info("Running VACUUM FULL.") Logger.warn( "Re-packing your entire database may take a while and will consume extra disk space during the process." diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex index 0d2e94248..b08b83305 100644 --- a/lib/pleroma/maps.ex +++ b/lib/pleroma/maps.ex @@ -12,4 +12,10 @@ def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(ma _ -> map end end + + def safe_put_in(data, keys, value) when is_map(data) and is_list(keys) do + Kernel.put_in(data, keys, value) + rescue + _ -> data + end end diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 1849cacc8..adb51d33a 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -338,6 +338,26 @@ def get_log_entry_message(%ModerationLog{ "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}" end + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "add_suggestion", + "subject" => users + } + }) do + "@#{actor_nickname} added suggested users: #{users_to_nicknames_string(users)}" + end + + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "remove_suggestion", + "subject" => users + } + }) do + "@#{actor_nickname} removed suggested users: #{users_to_nicknames_string(users)}" + end + def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, @@ -481,9 +501,7 @@ def get_log_entry_message(%ModerationLog{ "visibility" => visibility } }) do - "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{ - visibility - }'" + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{visibility}'" end def get_log_entry_message(%ModerationLog{ @@ -523,9 +541,7 @@ def get_log_entry_message(%ModerationLog{ "subject" => subjects } }) do - "@#{actor_nickname} re-sent confirmation email for users: #{ - users_to_nicknames_string(subjects) - }" + "@#{actor_nickname} re-sent confirmation email for users: #{users_to_nicknames_string(subjects)}" end def get_log_entry_message(%ModerationLog{ diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 7efbdc49a..9e0ce0329 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -72,6 +72,7 @@ def unread_notifications_count(%User{id: user_id}) do pleroma:emoji_reaction pleroma:report reblog + poll } def changeset(%Notification{} = notification, attrs) do @@ -127,6 +128,7 @@ def for_user_query(user, opts \\ %{}) do |> where([user_actor: user_actor], user_actor.is_active) |> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_blocked(user, exclude_blocked_opts) + |> exclude_blockers(user) |> exclude_filtered(user) |> exclude_visibility(opts) end @@ -140,6 +142,17 @@ defp exclude_blocked(query, user, opts) do |> FollowingRelationship.keep_following_or_not_domain_blocked(user) end + defp exclude_blockers(query, user) do + if Pleroma.Config.get([:activitypub, :blockers_visible]) == true do + query + else + blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block]) + + query + |> where([n, a], a.actor not in ^blocker_ap_ids) + end + end + defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do query end @@ -379,7 +392,7 @@ defp do_create_notifications(%Activity{} = activity, options) do notifications = Enum.map(potential_receivers, fn user -> do_send = do_send && user in enabled_receivers - create_notification(activity, user, do_send) + create_notification(activity, user, do_send: do_send) end) |> Enum.reject(&is_nil/1) @@ -435,15 +448,18 @@ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do end # TODO move to sql, too. - def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do - unless skip?(activity, user) do + def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do + do_send = Keyword.get(opts, :do_send, true) + type = Keyword.get(opts, :type, type_from_activity(activity)) + + unless skip?(activity, user, opts) do {:ok, %{notification: notification}} = Multi.new() |> Multi.insert(:notification, %Notification{ user_id: user.id, activity: activity, seen: mark_as_read?(activity, user), - type: type_from_activity(activity) + type: type }) |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() @@ -457,6 +473,28 @@ def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) end end + def create_poll_notifications(%Activity{} = activity) do + with %Object{data: %{"type" => "Question", "actor" => actor} = data} <- + Object.normalize(activity) do + voters = + case data do + %{"voters" => voters} when is_list(voters) -> voters + _ -> [] + end + + notifications = + Enum.reduce([actor | voters], [], fn ap_id, acc -> + with %User{local: true} = user <- User.get_by_ap_id(ap_id) do + [create_notification(activity, user, type: "poll") | acc] + else + _ -> acc + end + end) + + {:ok, notifications} + end + end + @doc """ Returns a tuple with 2 elements: {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)} @@ -572,8 +610,10 @@ def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do Enum.uniq(ap_ids) -- thread_muter_ap_ids end - @spec skip?(Activity.t(), User.t()) :: boolean() - def skip?(%Activity{} = activity, %User{} = user) do + def skip?(activity, user, opts \\ []) + + @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean() + def skip?(%Activity{} = activity, %User{} = user, opts) do [ :self, :invisible, @@ -581,17 +621,21 @@ def skip?(%Activity{} = activity, %User{} = user) do :recently_followed, :filtered ] - |> Enum.find(&skip?(&1, activity, user)) + |> Enum.find(&skip?(&1, activity, user, opts)) end - def skip?(_, _), do: false + def skip?(_activity, _user, _opts), do: false - @spec skip?(atom(), Activity.t(), User.t()) :: boolean() - def skip?(:self, %Activity{} = activity, %User{} = user) do - activity.data["actor"] == user.ap_id + @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean() + def skip?(:self, %Activity{} = activity, %User{} = user, opts) do + cond do + opts[:type] == "poll" -> false + activity.data["actor"] == user.ap_id -> true + true -> false + end end - def skip?(:invisible, %Activity{} = activity, _) do + def skip?(:invisible, %Activity{} = activity, _user, _opts) do actor = activity.data["actor"] user = User.get_cached_by_ap_id(actor) User.invisible?(user) @@ -600,15 +644,27 @@ def skip?(:invisible, %Activity{} = activity, _) do def skip?( :block_from_strangers, %Activity{} = activity, - %User{notification_settings: %{block_from_strangers: true}} = user + %User{notification_settings: %{block_from_strangers: true}} = user, + opts ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) - !User.following?(follower, user) + + cond do + opts[:type] == "poll" -> false + user.ap_id == actor -> false + !User.following?(follower, user) -> true + true -> false + end 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 + def skip?( + :recently_followed, + %Activity{data: %{"type" => "Follow"}} = activity, + %User{} = user, + _opts + ) do actor = activity.data["actor"] Notification.for_user(user) @@ -618,9 +674,10 @@ 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, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"], + do: false - def skip?(:filtered, activity, user) do + def skip?(:filtered, activity, user, _opts) do object = Object.normalize(activity, fetch: false) cond do @@ -638,7 +695,7 @@ def skip?(:filtered, activity, user) do end end - def skip?(_, _, _), do: false + def skip?(_type, _activity, _user, _opts), do: false def mark_as_read?(activity, target_user) do user = Activity.user_actor(activity) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 3ba749d1a..c3ea1b98b 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -366,7 +366,7 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do end def local?(%Object{data: %{"id" => id}}) do - String.starts_with?(id, Pleroma.Web.base_url() <> "/") + String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/") end def replies(object, opts \\ []) do diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index bcccf1c4c..4ca67f0fd 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.HTTP + alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -101,6 +102,9 @@ def fetch_object_from_id(id, options \\ []) do {:transmogrifier, {:error, {:reject, e}}} -> {:reject, e} + {:transmogrifier, {:reject, e}} -> + {:reject, e} + {:transmogrifier, _} = e -> {:error, e} @@ -124,12 +128,14 @@ def fetch_object_from_id(id, options \\ []) do defp prepare_activity_params(data) do %{ "type" => "Create", - "to" => data["to"] || [], - "cc" => data["cc"] || [], # Should we seriously keep this attributedTo thing? "actor" => data["actor"] || data["attributedTo"], "object" => data } + |> Maps.put_if_present("to", data["to"]) + |> Maps.put_if_present("cc", data["cc"]) + |> Maps.put_if_present("bto", data["bto"]) + |> Maps.put_if_present("bcc", data["bcc"]) end def fetch_object_from_id!(id, options \\ []) do diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index b8ea06e33..61b64ed3e 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,8 +8,6 @@ defmodule Pleroma.Repo do adapter: Ecto.Adapters.Postgres, migration_timestamps: [type: :naive_datetime_usec] - use Ecto.Explain - import Ecto.Query require Logger diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 406f7e2b8..ec69a1779 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -411,7 +411,7 @@ defp increase_read_duration(_) do {:ok, :no_duration_limit, :no_duration_limit} end - defp client, do: Pleroma.ReverseProxy.Client + defp client, do: Pleroma.ReverseProxy.Client.Wrapper defp track_failed_url(url, error, opts) do ttl = diff --git a/lib/pleroma/reverse_proxy/client.ex b/lib/pleroma/reverse_proxy/client.ex index 8698fa2e1..75243d2dc 100644 --- a/lib/pleroma/reverse_proxy/client.ex +++ b/lib/pleroma/reverse_proxy/client.ex @@ -17,22 +17,4 @@ defmodule Pleroma.ReverseProxy.Client do @callback stream_body(map()) :: {:ok, binary(), map()} | :done | {:error, atom() | String.t()} @callback close(reference() | pid() | map()) :: :ok - - def request(method, url, headers, body \\ "", opts \\ []) do - client().request(method, url, headers, body, opts) - end - - def stream_body(ref), do: client().stream_body(ref) - - def close(ref), do: client().close(ref) - - defp client do - :tesla - |> Application.get_env(:adapter) - |> client() - end - - defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney - defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla - defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client) end diff --git a/lib/pleroma/reverse_proxy/client/wrapper.ex b/lib/pleroma/reverse_proxy/client/wrapper.ex new file mode 100644 index 000000000..06dd29fea --- /dev/null +++ b/lib/pleroma/reverse_proxy/client/wrapper.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReverseProxy.Client.Wrapper do + @moduledoc "Meta-client that calls the appropriate client from the config." + @behaviour Pleroma.ReverseProxy.Client + + @impl true + def request(method, url, headers, body \\ "", opts \\ []) do + client().request(method, url, headers, body, opts) + end + + @impl true + def stream_body(ref), do: client().stream_body(ref) + + @impl true + def close(ref), do: client().close(ref) + + defp client do + :tesla + |> Application.get_env(:adapter) + |> client() + end + + defp client(Tesla.Adapter.Hackney), do: Pleroma.ReverseProxy.Client.Hackney + defp client(Tesla.Adapter.Gun), do: Pleroma.ReverseProxy.Client.Tesla + defp client(_), do: Pleroma.Config.get!(Pleroma.ReverseProxy.Client) +end diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex index 44d2f48dc..10165c1b2 100644 --- a/lib/pleroma/telemetry/logger.ex +++ b/lib/pleroma/telemetry/logger.ex @@ -29,9 +29,7 @@ def handle_event( _ ) do Logger.debug(fn -> - "Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{ - reclaim_max - } connections" + "Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{reclaim_max} connections" end) end @@ -73,9 +71,7 @@ def handle_event( _ ) do Logger.warn(fn -> - "Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{ - inspect(reason) - }" + "Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{inspect(reason)}" end) end diff --git a/lib/pleroma/tests/auth_test_controller.ex b/lib/pleroma/tests/auth_test_controller.ex index ddf3fea4f..76514948b 100644 --- a/lib/pleroma/tests/auth_test_controller.ex +++ b/lib/pleroma/tests/auth_test_controller.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Tests.AuthTestController do use Pleroma.Web, :controller alias Pleroma.User - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug # Serves only with proper OAuth token (:api and :authenticated_api) @@ -47,10 +46,7 @@ defmodule Pleroma.Tests.AuthTestController do # Via :authenticated_api, serves if token is present and has requested scopes # # Suggested use: as :fallback_oauth_check but open with nil :user for :api on private instances - plug( - :skip_plug, - EnsurePublicOrAuthenticatedPlug when action == :fallback_oauth_skip_publicity_check - ) + plug(:skip_public_check when action == :fallback_oauth_skip_publicity_check) plug( OAuthScopesPlug, @@ -62,11 +58,7 @@ defmodule Pleroma.Tests.AuthTestController do # Via :authenticated_api, serves if :user is set (regardless of token presence and its scopes) # # Suggested use: making an :api endpoint always accessible (e.g. email confirmation endpoint) - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] - when action == :skip_oauth_skip_publicity_check - ) + plug(:skip_auth when action == :skip_oauth_skip_publicity_check) # Via :authenticated_api, always fails with 403 (endpoint is insecure) # Via :api, drops :user if present and serves if public (private instance rejects on no user) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 654711351..17822dc5e 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -23,6 +23,9 @@ defmodule Pleroma.Upload do is once created permanent and changing it (especially in uploaders) is probably a bad idea! * `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over. + * `:width` - width of the media in pixels + * `:height` - height of the media in pixels + * `:blurhash` - string hash of the image encoded with the blurhash algorithm (https://blurha.sh/) Related behaviors: @@ -32,6 +35,7 @@ defmodule Pleroma.Upload do """ alias Ecto.UUID alias Pleroma.Config + alias Pleroma.Maps require Logger @type source :: @@ -53,9 +57,12 @@ defmodule Pleroma.Upload do name: String.t(), tempfile: String.t(), content_type: String.t(), + width: integer(), + height: integer(), + blurhash: String.t(), path: String.t() } - defstruct [:id, :name, :tempfile, :content_type, :path] + defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path] defp get_description(opts, upload) do case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do @@ -89,9 +96,12 @@ def store(upload, opts \\ []) do "mediaType" => upload.content_type, "href" => url_from_spec(upload, opts.base_url, url_spec) } + |> Maps.put_if_present("width", upload.width) + |> Maps.put_if_present("height", upload.height) ], "name" => description - }} + } + |> Maps.put_if_present("blurhash", upload.blurhash)} else {:description_limit, _} -> {:error, :description_too_long} @@ -225,7 +235,7 @@ def base_url do case uploader do Pleroma.Uploaders.Local -> - upload_base_url || Pleroma.Web.base_url() <> "/media/" + upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" Pleroma.Uploaders.S3 -> bucket = Config.get([Pleroma.Uploaders.S3, :bucket]) @@ -251,7 +261,7 @@ def base_url do end _ -> - public_endpoint || upload_base_url || Pleroma.Web.base_url() <> "/media/" + public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" end end end diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex index c677d4b9f..e5db2fb20 100644 --- a/lib/pleroma/upload/filter.ex +++ b/lib/pleroma/upload/filter.ex @@ -15,13 +15,13 @@ defmodule Pleroma.Upload.Filter do require Logger - @callback filter(Pleroma.Upload.t()) :: + @callback filter(upload :: struct()) :: {:ok, :filtered} | {:ok, :noop} - | {:ok, :filtered, Pleroma.Upload.t()} + | {:ok, :filtered, upload :: struct()} | {:error, any()} - @spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.Upload.t()} | {:error, any()} + @spec filter([module()], upload :: struct()) :: {:ok, upload :: struct()} | {:error, any()} def filter([], upload) do {:ok, upload} diff --git a/lib/pleroma/upload/filter/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex new file mode 100644 index 000000000..c89c30fc1 --- /dev/null +++ b/lib/pleroma/upload/filter/analyze_metadata.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.AnalyzeMetadata do + @moduledoc """ + Extracts metadata about the upload, such as width/height + """ + require Logger + + @behaviour Pleroma.Upload.Filter + + @spec filter(Pleroma.Upload.t()) :: + {:ok, :filtered, Pleroma.Upload.t()} | {:ok, :noop} | {:error, String.t()} + def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do + try do + image = + file + |> Mogrify.open() + |> Mogrify.verbose() + + upload = + upload + |> Map.put(:width, image.width) + |> Map.put(:height, image.height) + |> Map.put(:blurhash, get_blurhash(file)) + + {:ok, :filtered, upload} + rescue + e in ErlangError -> + Logger.warn("#{__MODULE__}: #{inspect(e)}") + {:ok, :noop} + end + end + + def filter(%Pleroma.Upload{tempfile: file, content_type: "video" <> _} = upload) do + try do + result = media_dimensions(file) + + upload = + upload + |> Map.put(:width, result.width) + |> Map.put(:height, result.height) + + {:ok, :filtered, upload} + rescue + e in ErlangError -> + Logger.warn("#{__MODULE__}: #{inspect(e)}") + {:ok, :noop} + end + end + + def filter(_), do: {:ok, :noop} + + defp get_blurhash(file) do + with {:ok, blurhash} <- :eblurhash.magick(file) do + blurhash + else + _ -> nil + end + end + + defp media_dimensions(file) do + with executable when is_binary(executable) <- System.find_executable("ffprobe"), + args = [ + "-v", + "error", + "-show_entries", + "stream=width,height", + "-of", + "csv=p=0:s=x", + file + ], + {result, 0} <- System.cmd(executable, args), + [width, height] <- + String.split(String.trim(result), "x") |> Enum.map(&String.to_integer(&1)) do + %{width: width, height: height} + else + nil -> {:error, {:ffprobe, :command_not_found}} + {:error, _} = error -> error + end + end +end diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index 0be878ca2..deba548b7 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Uploaders.Uploader do """ @type file_spec :: {:file | :url, String.t()} - @callback put_file(Pleroma.Upload.t()) :: + @callback put_file(upload :: struct()) :: :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback @callback delete_file(file :: String.t()) :: :ok | {:error, String.t()} @@ -46,7 +46,7 @@ defmodule Pleroma.Uploaders.Uploader do | {:error, Plug.Conn.t(), String.t()} @optional_callbacks http_callback: 2 - @spec put_file(module(), Pleroma.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()} + @spec put_file(module(), upload :: struct()) :: {:ok, file_spec()} | {:error, String.t()} def put_file(uploader, upload) do case uploader.put_file(upload) do :ok -> {:ok, {:file, upload.path}} diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index b78777141..c25023dc1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -27,13 +27,13 @@ defmodule Pleroma.User do alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserRelationship - alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils + alias Pleroma.Web.Endpoint alias Pleroma.Web.OAuth alias Pleroma.Web.RelMe alias Pleroma.Workers.BackgroundWorker @@ -124,7 +124,6 @@ defmodule Pleroma.User do field(:is_moderator, :boolean, default: false) field(:is_admin, :boolean, default: false) field(:show_role, :boolean, default: true) - 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) @@ -149,6 +148,7 @@ defmodule Pleroma.User do field(:last_active_at, :naive_datetime) field(:disclose_client, :boolean, default: true) field(:pinned_objects, :map, default: %{}) + field(:is_suggested, :boolean, default: false) embeds_one( :notification_settings, @@ -360,7 +360,7 @@ def avatar_url(user, options \\ []) do _ -> unless options[:no_default] do - Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png") + Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png") end end end @@ -368,13 +368,13 @@ def avatar_url(user, options \\ []) do def banner_url(user, options \\ []) do case user.banner do %{"url" => [%{"href" => href} | _]} -> href - _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png" + _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png" end end # Should probably be renamed or removed @spec ap_id(User.t()) :: String.t() - def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" + def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}" @spec ap_followers(User.t()) :: String.t() def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa @@ -1677,6 +1677,22 @@ def confirm(%User{is_confirmed: false} = user) do def confirm(%User{} = user), do: {:ok, user} + def set_suggestion(users, is_suggested) when is_list(users) do + Repo.transaction(fn -> + Enum.map(users, fn user -> + with {:ok, user} <- set_suggestion(user, is_suggested), do: user + end) + end) + end + + def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user} + + def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do + user + |> change(is_suggested: is_suggested) + |> update_and_set_cache() + end + def update_notification_settings(%User{} = user, settings) do user |> cast(%{notification_settings: settings}, []) @@ -1695,8 +1711,6 @@ def purge_user_changeset(user) do email: nil, name: nil, password_hash: nil, - keys: nil, - public_key: nil, avatar: %{}, tags: [], last_refreshed_at: nil, @@ -1707,9 +1721,7 @@ def purge_user_changeset(user) do follower_count: 0, following_count: 0, is_locked: false, - is_confirmed: true, password_reset_pending: false, - is_approved: true, registration_reason: nil, confirmation_token: nil, domain_blocks: [], @@ -1717,7 +1729,6 @@ def purge_user_changeset(user) do ap_enabled: false, is_moderator: false, is_admin: false, - mastofe_settings: nil, mascot: nil, emoji: %{}, pleroma_settings_store: %{}, @@ -1725,45 +1736,53 @@ def purge_user_changeset(user) do raw_fields: [], is_discoverable: false, also_known_as: [] + # id: preserved + # ap_id: preserved + # nickname: preserved }) end + # Purge doesn't delete the user from the database. + # It just nulls all its fields and deactivates it. + # See `User.purge_user_changeset/1` above. + defp purge(%User{} = user) do + user + |> purge_user_changeset() + |> update_and_set_cache() + end + def delete(users) when is_list(users) do for user <- users, do: delete(user) end def delete(%User{} = user) do + # Purge the user immediately + purge(user) BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) end - defp delete_and_invalidate_cache(%User{} = user) do + # *Actually* delete the user from the DB + defp delete_from_db(%User{} = user) do invalidate_cache(user) Repo.delete(user) end - defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user) + # If the user never finalized their account, it's safe to delete them. + defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user), + do: delete_from_db(user) - defp delete_or_deactivate(%User{local: true} = user) do - status = account_status(user) + defp maybe_delete_from_db(%User{local: true, is_approved: false} = user), + do: delete_from_db(user) - case status do - :confirmation_pending -> - delete_and_invalidate_cache(user) - - :approval_pending -> - delete_and_invalidate_cache(user) - - _ -> - user - |> purge_user_changeset() - |> update_and_set_cache() - end - end + defp maybe_delete_from_db(user), do: {:ok, user} def perform(:force_password_reset, user), do: force_password_reset(user) @spec perform(atom(), User.t()) :: {:ok, User.t()} def perform(:delete, %User{} = user) do + # Purge the user again, in case perform/2 is called directly + purge(user) + # Remove all relationships user |> get_followers() @@ -1781,10 +1800,9 @@ def perform(:delete, %User{} = user) do delete_user_activities(user) delete_notifications_from_user_activities(user) - delete_outgoing_pending_follow_requests(user) - delete_or_deactivate(user) + maybe_delete_from_db(user) end def perform(:set_activation_async, user, status), do: set_activation(user, status) @@ -2245,7 +2263,7 @@ def get_delivered_users_by_object_id(object_id) do def change_email(user, email) do user |> cast(%{email: email}, [:email]) - |> validate_required([:email]) + |> maybe_validate_required_email(false) |> unique_constraint(:email) |> validate_format(:email, @email_regex) |> update_and_set_cache() @@ -2328,13 +2346,6 @@ def mascot_update(user, url) do |> update_and_set_cache() end - def mastodon_settings_update(user, settings) do - user - |> cast(%{mastofe_settings: settings}, [:mastofe_settings]) - |> validate_required([:mastofe_settings]) - |> update_and_set_cache() - end - @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t() def confirmation_changeset(user, set_confirmation: confirmed?) do params = @@ -2480,8 +2491,8 @@ def update_last_active_at(%__MODULE__{local: true} = user) do |> update_and_set_cache() end - def active_user_count(weeks \\ 4) do - active_after = Timex.shift(NaiveDateTime.utc_now(), weeks: -weeks) + def active_user_count(days \\ 30) do + active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days) __MODULE__ |> where([u], u.last_active_at >= ^active_after) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index fa46545da..6d4a4ead6 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -27,7 +27,7 @@ defmodule Pleroma.User.Query do - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]}) """ import Ecto.Query - import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] alias Pleroma.FollowingRelationship alias Pleroma.User @@ -46,6 +46,7 @@ defmodule Pleroma.User.Query do unconfirmed: boolean(), is_admin: boolean(), is_moderator: boolean(), + is_suggested: boolean(), super_users: boolean(), invisible: boolean(), internal: boolean(), @@ -167,6 +168,10 @@ defp compose_query({:unconfirmed, _}, query) do where(query, [u], u.is_confirmed == false) end + defp compose_query({:is_suggested, bool}, query) do + where(query, [u], u.is_suggested == ^bool) + end + defp compose_query({:followers, %User{id: id}}, query) do query |> where([u], u.id != ^id) diff --git a/lib/pleroma/user_note.ex b/lib/pleroma/user_note.ex new file mode 100644 index 000000000..5e82d359f --- /dev/null +++ b/lib/pleroma/user_note.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserNote do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserNote + + schema "user_notes" do + belongs_to(:source, User, type: FlakeId.Ecto.CompatType) + belongs_to(:target, User, type: FlakeId.Ecto.CompatType) + field(:comment, :string) + + timestamps() + end + + def changeset(%UserNote{} = user_note, params \\ %{}) do + user_note + |> cast(params, [:source_id, :target_id, :comment]) + |> validate_required([:source_id, :target_id]) + end + + def show(%User{} = source, %User{} = target) do + with %UserNote{} = note <- + UserNote + |> where(source_id: ^source.id, target_id: ^target.id) + |> Repo.one() do + note.comment + else + _ -> "" + end + end + + def create(%User{} = source, %User{} = target, comment) do + %UserNote{} + |> changeset(%{ + source_id: source.id, + target_id: target.id, + comment: comment + }) + |> Repo.insert( + on_conflict: {:replace, [:comment]}, + conflict_target: [:source_id, :target_id] + ) + end +end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 8630f244b..5761e3b38 100644 --- a/lib/pleroma/web.ex +++ b/lib/pleroma/web.ex @@ -35,9 +35,10 @@ def controller do import Plug.Conn import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers import Pleroma.Web.TranslationHelpers + alias Pleroma.Web.Router.Helpers, as: Routes + plug(:set_put_layout) defp set_put_layout(conn, _) do @@ -61,6 +62,14 @@ defp skip_plug(conn, plug_modules) do ) end + defp skip_auth(conn, _) do + skip_plug(conn, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug]) + end + + defp skip_public_check(conn, _) do + skip_plug(conn, EnsurePublicOrAuthenticatedPlug) + end + # Executed just before actual controller action, invokes before-action hooks (callbacks) defp action(conn, params) do with %{halted: false} = conn <- @@ -131,7 +140,8 @@ def view do import Pleroma.Web.ErrorHelpers import Pleroma.Web.Gettext - import Pleroma.Web.Router.Helpers + + alias Pleroma.Web.Router.Helpers, as: Routes require Logger @@ -229,20 +239,4 @@ def call(%Plug.Conn{} = conn, options) do defmacro __using__(which) when is_atom(which) do apply(__MODULE__, which, []) end - - def base_url do - Pleroma.Web.Endpoint.url() - end - - # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+ - def get_api_routes do - Pleroma.Web.Router.__routes__() - |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end) - |> Enum.map(fn r -> - r.path - |> String.split("/", trim: true) - |> List.first() - end) - |> Enum.uniq() - end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d0051d1cb..8324ca22c 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Web.Streamer alias Pleroma.Web.WebFinger alias Pleroma.Workers.BackgroundWorker + alias Pleroma.Workers.PollWorker import Ecto.Query import Pleroma.Web.ActivityPub.Utils @@ -53,15 +54,18 @@ defp get_recipients(data) do {recipients, to, cc} end - defp check_actor_is_active(nil), do: true + defp check_actor_can_insert(%{"type" => "Delete"}), do: true + defp check_actor_can_insert(%{"type" => "Undo"}), do: true - defp check_actor_is_active(actor) when is_binary(actor) do + defp check_actor_can_insert(%{"actor" => actor}) when is_binary(actor) do case User.get_cached_by_ap_id(actor) do %User{is_active: true} -> true _ -> false end end + defp check_actor_can_insert(_), do: true + defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do limit = Config.get([:instance, :remote_limit]) String.length(content) <= limit @@ -88,7 +92,7 @@ defp increase_replies_count_if_reply(%{ defp increase_replies_count_if_reply(_create_data), do: :noop - @object_types ~w[ChatMessage Question Answer Audio Video Event Article] + @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page] @impl true def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do @@ -117,7 +121,7 @@ def persist(object, meta) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), - {_, true} <- {:actor_check, bypass_actor_check || check_actor_is_active(map["actor"])}, + {_, true} <- {:actor_check, bypass_actor_check || check_actor_can_insert(map)}, {_, true} <- {:remote_limit_pass, check_remote_limit(map)}, {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), @@ -285,6 +289,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity}, {:ok, _actor} <- increase_note_count_if_public(actor, activity), _ <- notify_and_stream(activity), + :ok <- maybe_schedule_poll_notifications(activity), :ok <- maybe_federate(activity) do {:ok, activity} else @@ -299,6 +304,11 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param end end + defp maybe_schedule_poll_notifications(activity) do + PollWorker.schedule_poll_end(activity) + :ok + end + @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()} def listen(%{to: to, actor: actor, context: context, object: object} = params) do additional = params[:additional] || %{} @@ -431,6 +441,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) + |> restrict_blockers_visibility(opts) |> restrict_recipients(recipients, opts[:user]) |> restrict_filtered(opts) |> where( @@ -1018,7 +1029,10 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do from( [activity, object: o] in query, + # You don't block the author where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), + + # You don't block any recipients, and didn't author the post where: fragment( "((not (? && ?)) or ? = ?)", @@ -1027,12 +1041,18 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do activity.actor, ^user.ap_id ), + + # You don't block the domain of any recipients, and didn't author the post where: fragment( - "recipients_contain_blocked_domains(?, ?) = false", + "(recipients_contain_blocked_domains(?, ?) = false) or ? = ?", activity.recipients, - ^domain_blocks + ^domain_blocks, + activity.actor, + ^user.ap_id ), + + # It's not a boost of a user you block where: fragment( "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", @@ -1040,6 +1060,8 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do activity.data, ^blocked_ap_ids ), + + # You don't block the author's domain, and also don't follow the author where: fragment( "(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)", @@ -1048,6 +1070,8 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do activity.actor, ^following_ap_ids ), + + # Same as above, but checks the Object where: fragment( "(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)", @@ -1061,6 +1085,31 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do defp restrict_blocked(query, _), do: query + defp restrict_blockers_visibility(query, %{blocking_user: %User{} = user}) do + if Config.get([:activitypub, :blockers_visible]) == true do + query + else + blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block]) + + from( + activity in query, + # The author doesn't block you + where: fragment("not (? = ANY(?))", activity.actor, ^blocker_ap_ids), + + # It's not a boost of a user that blocks you + where: + fragment( + "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", + activity.data, + activity.data, + ^blocker_ap_ids + ) + ) + end + end + + defp restrict_blockers_visibility(query, _), do: query + defp restrict_unlisted(query, %{restrict_unlisted: true}) do from( activity in query, @@ -1287,6 +1336,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_state(opts) |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) + |> restrict_blockers_visibility(opts) |> restrict_muted(restrict_muted_opts) |> restrict_filtered(opts) |> restrict_media(opts) @@ -1587,9 +1637,7 @@ def maybe_handle_clashing_nickname(data) do %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." + "Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{data[:ap_id]}, renaming." ) old_user diff --git a/lib/pleroma/web/activity_pub/activity_pub/persisting.ex b/lib/pleroma/web/activity_pub/activity_pub/persisting.ex index 5ec8b7bab..f39cd000a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub/persisting.ex +++ b/lib/pleroma/web/activity_pub/activity_pub/persisting.ex @@ -3,5 +3,5 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ActivityPub.Persisting do - @callback persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} + @callback persist(map(), keyword()) :: {:ok, struct()} end diff --git a/lib/pleroma/web/activity_pub/activity_pub/streaming.ex b/lib/pleroma/web/activity_pub/activity_pub/streaming.ex index 983168bff..33c7bf2bc 100644 --- a/lib/pleroma/web/activity_pub/activity_pub/streaming.ex +++ b/lib/pleroma/web/activity_pub/activity_pub/streaming.ex @@ -3,10 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ActivityPub.Streaming do - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.User - - @callback stream_out(Activity.t()) :: any() - @callback stream_out_participations(Object.t(), User.t()) :: any() + @callback stream_out(struct()) :: any() + @callback stream_out_participations(struct(), struct()) :: any() end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 5aa3b281a..4a19938f6 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Object.Fetcher alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Pipeline @@ -284,15 +283,29 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do json(conn, "ok") end + def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do + conn + |> put_status(:bad_request) + |> json("Invalid HTTP Signature") + end + # POST /relay/inbox -or- POST /internal/fetch/inbox - def inbox(conn, params) do - if params["type"] == "Create" && FederatingPlug.federating?() do + def inbox(conn, %{"type" => "Create"} = params) do + if FederatingPlug.federating?() do post_inbox_relayed_create(conn, params) else - post_inbox_fallback(conn, params) + conn + |> put_status(:bad_request) + |> json("Not federating") end end + def inbox(conn, _params) do + conn + |> put_status(:bad_request) + |> json("error, missing HTTP Signature") + end + defp post_inbox_relayed_create(conn, params) do Logger.debug( "Signature missing or not from author, relayed Create message, fetching object from source" @@ -303,23 +316,6 @@ defp post_inbox_relayed_create(conn, params) do json(conn, "ok") end - defp post_inbox_fallback(conn, params) do - headers = Enum.into(conn.req_headers, %{}) - - if headers["signature"] && params["actor"] && - String.contains?(headers["signature"], params["actor"]) do - Logger.debug( - "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!" - ) - - Logger.debug(inspect(conn.req_headers)) - end - - conn - |> put_status(:bad_request) - |> json(dgettext("errors", "error")) - end - defp represent_service_actor(%User{} = user, conn) do with {:ok, user} <- User.ensure_keys_present(user) do conn @@ -403,83 +399,90 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{ |> json(err) end - defp handle_user_activity( - %User{} = user, - %{"type" => "Create", "object" => %{"type" => "Note"} = object} = params - ) do - content = if is_binary(object["content"]), do: object["content"], else: "" - name = if is_binary(object["name"]), do: object["name"], else: "" - summary = if is_binary(object["summary"]), do: object["summary"], else: "" - length = String.length(content <> name <> summary) + defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity) + when is_map(object) do + length = + [object["content"], object["summary"], object["name"]] + |> Enum.filter(&is_binary(&1)) + |> Enum.join("") + |> String.length() - if length > Pleroma.Config.get([:instance, :limit]) do - {:error, dgettext("errors", "Note is over the character limit")} - else + limit = Pleroma.Config.get([:instance, :limit]) + + if length < limit do object = object - |> Map.merge(Map.take(params, ["to", "cc"])) - |> Map.put("attributedTo", user.ap_id) - |> Transmogrifier.fix_object() + |> Transmogrifier.strip_internal_fields() + |> Map.put("attributedTo", actor) + |> Map.put("actor", actor) + |> Map.put("id", Utils.generate_object_id()) - ActivityPub.create(%{ - to: params["to"], - actor: user, - context: object["context"], - object: object, - additional: Map.take(params, ["cc"]) - }) - end - end - - defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do - with %Object{} = object <- Object.normalize(params["object"], fetch: false), - true <- user.is_moderator || user.ap_id == object.data["actor"], - {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), - {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do - {:ok, delete} + {:ok, Map.put(activity, "object", object)} else - _ -> {:error, dgettext("errors", "Can't delete object")} + {:error, + dgettext( + "errors", + "Character limit (%{limit} characters) exceeded, contains %{length} characters", + limit: limit, + length: length + )} end end - defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do - with %Object{} = object <- Object.normalize(params["object"], fetch: false), - {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, - {_, {:ok, %Activity{} = activity, _meta}} <- - {:common_pipeline, - Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do + defp fix_user_message( + %User{ap_id: actor} = user, + %{"type" => "Delete", "object" => object} = activity + ) do + with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)}, + {_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do {:ok, activity} else - _ -> {:error, dgettext("errors", "Can't like object")} + {:normalize, _} -> + {:error, "No such object found"} + + {:permission, _} -> + {:forbidden, "You can't delete this object"} end end - defp handle_user_activity(_, _) do - {:error, dgettext("errors", "Unhandled activity type")} + defp fix_user_message(%User{}, activity) do + {:ok, activity} end def update_outbox( - %{assigns: %{user: %User{nickname: nickname} = user}} = conn, + %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn, %{"nickname" => nickname} = params ) do - actor = user.ap_id - params = params - |> Map.drop(["id"]) + |> Map.drop(["nickname"]) + |> Map.put("id", Utils.generate_activity_id()) |> Map.put("actor", actor) - |> Transmogrifier.fix_addressing() - with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do + with {:ok, params} <- fix_user_message(user, params), + {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true), + %Activity{data: activity_data} <- Activity.normalize(activity) do conn |> put_status(:created) - |> put_resp_header("location", activity.data["id"]) - |> json(activity.data) + |> put_resp_header("location", activity_data["id"]) + |> json(activity_data) else + {:forbidden, message} -> + conn + |> put_status(:forbidden) + |> json(message) + {:error, message} -> conn |> put_status(:bad_request) |> json(message) + + e -> + Logger.warn(fn -> "AP C2S: #{inspect(e)}" end) + + conn + |> put_status(:bad_request) + |> json("Bad Request") end end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 91a45836f..647ccf432 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI.ActivityDraft require Pleroma.Constants @@ -125,6 +126,37 @@ def create(actor, object, recipients) do |> Pleroma.Maps.put_if_present("context", context), []} end + @spec note(ActivityDraft.t()) :: {:ok, map(), keyword()} + def note(%ActivityDraft{} = draft) do + data = + %{ + "type" => "Note", + "to" => draft.to, + "cc" => draft.cc, + "content" => draft.content_html, + "summary" => draft.summary, + "sensitive" => draft.sensitive, + "context" => draft.context, + "attachment" => draft.attachments, + "actor" => draft.user.ap_id, + "tag" => Keyword.values(draft.tags) |> Enum.uniq() + } + |> add_in_reply_to(draft.in_reply_to) + |> Map.merge(draft.extra) + + {:ok, data, []} + end + + defp add_in_reply_to(object, nil), do: object + + defp add_in_reply_to(object, in_reply_to) do + with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do + Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) + else + _ -> object + end + end + def chat_message(actor, recipient, content, opts \\ []) do basic = %{ "id" => Utils.generate_object_id(), @@ -223,7 +255,7 @@ def announce(actor, object, options \\ []) do [actor.follower_address] public? and Visibility.is_local_public?(object) -> - [actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()] + [actor.follower_address, object.data["actor"], Utils.as_local_public()] public? -> [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index f2fec3ff6..bd6f6777f 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do 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} + suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF.Policy} }, %{ key: :transparency, @@ -33,9 +33,11 @@ defmodule Pleroma.Web.ActivityPub.MRF do %{ key: :transparency_exclusions, label: "MRF transparency exclusions", - type: {:list, :string}, + type: {:list, :tuple}, + key_placeholder: "instance", + value_placeholder: "reason", description: - "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", + "Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. You can also provide a reason for excluding these instance names. The instances and reasons won't be publicly disclosed.", suggestions: [ "exclusion.com" ] @@ -51,17 +53,6 @@ defmodule Pleroma.Web.ActivityPub.MRF do @required_description_keys [:key, :related_policy] - @callback filter(Map.t()) :: {:ok | :reject, Map.t()} - @callback describe() :: {:ok | :error, Map.t()} - @callback config_description() :: %{ - optional(:children) => [map()], - key: atom(), - related_policy: String.t(), - label: String.t(), - description: String.t() - } - @optional_callbacks config_description: 0 - def filter(policies, %{} = message) do policies |> Enum.reduce({:ok, message}, fn @@ -111,6 +102,11 @@ def subdomain_match?(domains, host) do Enum.any?(domains, fn domain -> Regex.match?(domain, host) end) end + @spec instance_list_from_tuples([{String.t(), String.t()}]) :: [String.t()] + def instance_list_from_tuples(list) do + Enum.map(list, fn {instance, _} -> instance end) + end + def describe(policies) do {:ok, policy_configs} = policies @@ -142,7 +138,7 @@ def describe(policies) do def describe, do: get_policies() |> describe() def config_descriptions do - Pleroma.Web.ActivityPub.MRF + Pleroma.Web.ActivityPub.MRF.Policy |> Pleroma.Docs.Generator.list_behaviour_implementations() |> config_descriptions() end @@ -161,9 +157,7 @@ def config_descriptions(policies) do [description | acc] else Logger.warn( - "#{policy} config description doesn't have one or all required keys #{ - inspect(@required_description_keys) - }" + "#{policy} config description doesn't have one or all required keys #{inspect(@required_description_keys)}" ) acc diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex index fc347236e..e78254280 100644 --- a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do @moduledoc "Adds expiration to all local Create activities" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(activity) do 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 b8bfdc3ce..851e95d22 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do @moduledoc "Prevent followbots from following with a bit of heuristic" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy # XXX: this should become User.normalize_by_ap_id() or similar, really. defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id) 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 40b19c3ab..cdf17fd28 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 @@ -5,7 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy require Logger diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index 378175205..b3ff86eed 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do require Logger @moduledoc "Drop and log everything received" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(object) do diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 2d3a10889..fad8d873b 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do alias Pleroma.Object @moduledoc "Ensure a re: is prepended on replies to a post with a Subject" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) diff --git a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex index 7307c9c14..7cf7de068 100644 --- a/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex @@ -1,5 +1,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.CommonAPI diff --git a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex index 51dbb1ad4..11871375e 100644 --- a/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/force_bot_unlisted_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @moduledoc "Remove bot posts from federated timeline" require Pleroma.Constants diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex index def0c437c..b7db4fa3d 100644 --- a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex @@ -14,7 +14,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists. """ - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp check_reject(message, hashtags) do if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 768a669f3..504bd4d57 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do @moduledoc "Block messages with too much mentions (configurable)" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp delist_message(message, threshold) when threshold > 0 do follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index f91b51bcf..1383fa757 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do @moduledoc "Reject or Word-Replace messages with a keyword or regex" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp string_matches?(string, _) when not is_binary(string) do false end @@ -159,6 +159,8 @@ def config_description do %{ key: :replace, type: {:list, :tuple}, + key_placeholder: "instance", + value_placeholder: "reason", description: """ **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`. diff --git a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex index 8dbf44071..25289d3a4 100644 --- a/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/media_proxy_warming_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do @moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy alias Pleroma.HTTP alias Pleroma.Web.MediaProxy diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex index 877277d4f..05b28e4f5 100644 --- a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do @moduledoc "Block messages which mention a user" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(%{"type" => "Create"} = message) do diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex index 32bb1b645..80bef591e 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex @@ -4,9 +4,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do @moduledoc "Filter local activities which have no content" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy - alias Pleroma.Web + alias Pleroma.Web.Endpoint @impl true def filter(%{"actor" => actor} = object) do @@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do def filter(object), do: {:ok, object} defp is_local?(actor) do - if actor |> String.starts_with?("#{Web.base_url()}") do + if actor |> String.starts_with?("#{Endpoint.url()}") do true else false diff --git a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex index 2ebc0674d..25031946c 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_op_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do @moduledoc "Does nothing (lets the messages go through unmodified)" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(object) do diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index b658d7d41..90272766c 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter( diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 2ad3fde0b..0d7146738 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do @moduledoc "Scrub configured hypertext markup" alias Pleroma.HTML - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(%{"type" => "Create", "object" => child_object} = object) do 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 aac24c0ec..02c9b18ed 100644 --- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do require Pleroma.Constants @moduledoc "Filter activities depending on their age" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp check_date(%{"object" => %{"published" => published}} = message) do with %DateTime{} = now <- DateTime.utc_now(), @@ -49,6 +49,8 @@ defp check_delist(message, actions) do message |> Map.put("to", to) |> Map.put("cc", cc) + |> Kernel.put_in(["object", "to"], to) + |> Kernel.put_in(["object", "cc"], cc) {:ok, message} else @@ -70,6 +72,8 @@ defp check_strip_followers(message, actions) do message |> Map.put("to", to) |> Map.put("cc", cc) + |> Kernel.put_in(["object", "to"], to) + |> Kernel.put_in(["object", "cc"], cc) {:ok, message} else @@ -82,7 +86,7 @@ defp check_strip_followers(message, actions) do end @impl true - def filter(%{"type" => "Create", "published" => _} = message) do + def filter(%{"type" => "Create", "object" => %{"published" => _}} = message) do with actions <- Config.get([:mrf_object_age, :actions]), {:reject, _} <- check_date(message), {:ok, message} <- check_reject(message, actions), diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex new file mode 100644 index 000000000..a4a960c01 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/policy.ex @@ -0,0 +1,16 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.Policy do + @callback filter(Map.t()) :: {:ok | :reject, Map.t()} + @callback describe() :: {:ok | :error, Map.t()} + @callback config_description() :: %{ + optional(:children) => [map()], + key: atom(), + related_policy: String.t(), + label: String.t(), + description: String.t() + } + @optional_callbacks config_description: 0 +end 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 47a43c6a2..dbb7ca0df 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do alias Pleroma.Config alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy require Pleroma.Constants @@ -47,7 +47,7 @@ def filter(object), do: {:ok, object} @impl true def describe, - do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} + do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Map.new()}} @impl true def config_description do diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 62024c58c..c631cc85f 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do @moduledoc "Filter activities depending on their origin instance" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy alias Pleroma.Config alias Pleroma.FollowingRelationship @@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do defp check_accept(%{host: actor_host} = _actor_info, object) do accepts = - Config.get([:mrf_simple, :accept]) + instance_list(:accept) |> MRF.subdomains_regex() cond do @@ -28,7 +28,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do defp check_reject(%{host: actor_host} = _actor_info, object) do rejects = - Config.get([:mrf_simple, :reject]) + instance_list(:reject) |> MRF.subdomains_regex() if MRF.subdomain_match?(rejects, actor_host) do @@ -44,7 +44,7 @@ defp check_media_removal( ) when length(child_attachment) > 0 do media_removal = - Config.get([:mrf_simple, :media_removal]) + instance_list(:media_removal) |> MRF.subdomains_regex() object = @@ -68,7 +68,7 @@ defp check_media_nsfw( } = object ) do media_nsfw = - Config.get([:mrf_simple, :media_nsfw]) + instance_list(:media_nsfw) |> MRF.subdomains_regex() object = @@ -85,7 +85,7 @@ defp check_media_nsfw(_actor_info, object), do: {:ok, object} defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do timeline_removal = - Config.get([:mrf_simple, :federated_timeline_removal]) + instance_list(:federated_timeline_removal) |> MRF.subdomains_regex() object = @@ -112,7 +112,7 @@ defp intersection(list1, list2) do defp check_followers_only(%{host: actor_host} = _actor_info, object) do followers_only = - Config.get([:mrf_simple, :followers_only]) + instance_list(:followers_only) |> MRF.subdomains_regex() object = @@ -137,7 +137,7 @@ defp check_followers_only(%{host: actor_host} = _actor_info, object) do defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do report_removal = - Config.get([:mrf_simple, :report_removal]) + instance_list(:report_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(report_removal, actor_host) do @@ -151,7 +151,7 @@ defp check_report_removal(_actor_info, object), do: {:ok, object} defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do avatar_removal = - Config.get([:mrf_simple, :avatar_removal]) + instance_list(:avatar_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(avatar_removal, actor_host) do @@ -165,7 +165,7 @@ defp check_avatar_removal(_actor_info, object), do: {:ok, object} defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do banner_removal = - Config.get([:mrf_simple, :banner_removal]) + instance_list(:banner_removal) |> MRF.subdomains_regex() if MRF.subdomain_match?(banner_removal, actor_host) do @@ -177,12 +177,25 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image defp check_banner_removal(_actor_info, object), do: {:ok, object} + defp check_object(%{"object" => object} = activity) do + with {:ok, _object} <- filter(object) do + {:ok, activity} + end + end + + defp check_object(object), do: {:ok, object} + + defp instance_list(config_key) do + Config.get([:mrf_simple, config_key]) + |> MRF.instance_list_from_tuples() + end + @impl true def filter(%{"type" => "Delete", "actor" => actor} = object) do %{host: actor_host} = URI.parse(actor) reject_deletes = - Config.get([:mrf_simple, :reject_deletes]) + instance_list(:reject_deletes) |> MRF.subdomains_regex() if MRF.subdomain_match?(reject_deletes, actor_host) do @@ -202,7 +215,8 @@ def filter(%{"actor" => actor} = object) do {:ok, object} <- check_media_nsfw(actor_info, object), {:ok, object} <- check_ftl_removal(actor_info, object), {:ok, object} <- check_followers_only(actor_info, object), - {:ok, object} <- check_report_removal(actor_info, object) do + {:ok, object} <- check_report_removal(actor_info, object), + {:ok, object} <- check_object(object) do {:ok, object} else {:reject, nil} -> {:reject, "[SimplePolicy]"} @@ -227,18 +241,59 @@ def filter(%{"id" => actor, "type" => obj_type} = object) end end + def filter(object) when is_binary(object) do + uri = URI.parse(object) + + with {:ok, object} <- check_accept(uri, object), + {:ok, object} <- check_reject(uri, object) do + {:ok, object} + else + {:reject, nil} -> {:reject, "[SimplePolicy]"} + {:reject, _} = e -> e + _ -> {:reject, "[SimplePolicy]"} + end + end + def filter(object), do: {:ok, object} @impl true def describe do - exclusions = Config.get([:mrf, :transparency_exclusions]) + exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples() + + mrf_simple_excluded = + Config.get(:mrf_simple) + |> Enum.map(fn {rule, instances} -> + {rule, Enum.reject(instances, fn {host, _} -> host in exclusions end)} + end) mrf_simple = - Config.get(:mrf_simple) - |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) - |> Enum.into(%{}) + mrf_simple_excluded + |> Enum.map(fn {rule, instances} -> + {rule, Enum.map(instances, fn {host, _} -> host end)} + end) + |> Map.new() - {:ok, %{mrf_simple: mrf_simple}} + # This is for backwards compatibility. We originally didn't sent + # extra info like a reason why an instance was rejected/quarantined/etc. + # Because we didn't want to break backwards compatibility it was decided + # to add an extra "info" key. + mrf_simple_info = + mrf_simple_excluded + |> Enum.map(fn {rule, instances} -> + {rule, Enum.reject(instances, fn {_, reason} -> reason == "" end)} + end) + |> Enum.reject(fn {_, instances} -> instances == [] end) + |> Enum.map(fn {rule, instances} -> + instances = + instances + |> Enum.map(fn {host, reason} -> {host, %{"reason" => reason}} end) + |> Map.new() + + {rule, instances} + end) + |> Map.new() + + {:ok, %{mrf_simple: mrf_simple, mrf_simple_info: mrf_simple_info}} end @impl true @@ -248,70 +303,67 @@ def config_description do related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy", label: "MRF Simple", description: "Simple ingress policies", - children: [ - %{ - key: :media_removal, - type: {:list, :string}, - description: "List of instances to strip media attachments from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :media_nsfw, - label: "Media NSFW", - type: {:list, :string}, - description: "List of instances to tag all media as NSFW (sensitive) from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :federated_timeline_removal, - type: {:list, :string}, - description: - "List of instances to remove from the Federated (aka The Whole Known Network) Timeline", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :reject, - type: {:list, :string}, - description: "List of instances to reject activities from (except deletes)", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :accept, - type: {:list, :string}, - description: "List of instances to only accept activities from (except deletes)", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :followers_only, - type: {:list, :string}, - description: "Force posts from the given instances to be visible by followers only", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :report_removal, - type: {:list, :string}, - description: "List of instances to reject reports from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :avatar_removal, - type: {:list, :string}, - description: "List of instances to strip avatars from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :banner_removal, - type: {:list, :string}, - description: "List of instances to strip banners from", - suggestions: ["example.com", "*.example.com"] - }, - %{ - key: :reject_deletes, - type: {:list, :string}, - description: "List of instances to reject deletions from", - suggestions: ["example.com", "*.example.com"] - } - ] + children: + [ + %{ + key: :media_removal, + description: + "List of instances to strip media attachments from and the reason for doing so" + }, + %{ + key: :media_nsfw, + label: "Media NSFW", + description: + "List of instances to tag all media as NSFW (sensitive) from and the reason for doing so" + }, + %{ + key: :federated_timeline_removal, + description: + "List of instances to remove from the Federated (aka The Whole Known Network) Timeline and the reason for doing so" + }, + %{ + key: :reject, + description: + "List of instances to reject activities from (except deletes) and the reason for doing so" + }, + %{ + key: :accept, + description: + "List of instances to only accept activities from (except deletes) and the reason for doing so" + }, + %{ + key: :followers_only, + description: + "Force posts from the given instances to be visible by followers only and the reason for doing so" + }, + %{ + key: :report_removal, + description: "List of instances to reject reports from and the reason for doing so" + }, + %{ + key: :avatar_removal, + description: "List of instances to strip avatars from and the reason for doing so" + }, + %{ + key: :banner_removal, + description: "List of instances to strip banners from and the reason for doing so" + }, + %{ + key: :reject_deletes, + description: "List of instances to reject deletions from and the reason for doing so" + } + ] + |> Enum.map(fn setting -> + Map.merge( + setting, + %{ + type: {:list, :tuple}, + key_placeholder: "instance", + value_placeholder: "reason", + suggestions: [{"example.com", "Some reason"}, {"*.example.com", "Another reason"}] + } + ) + end) } end end diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 4c5e33619..0dd415732 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do alias Pleroma.Config @moduledoc "Detect new emojis by their shortcode and steals them" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) @@ -38,9 +38,7 @@ defp steal_emoji({shortcode, url}, emoji_dir_path) do end else Logger.debug( - "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{ - size_limit - } B)" + "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)" ) nil @@ -92,6 +90,51 @@ def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = messa def filter(message), do: {:ok, message} + @impl true + @spec config_description :: %{ + children: [ + %{ + description: <<_::272, _::_*256>>, + key: :hosts | :rejected_shortcodes | :size_limit, + suggestions: [any(), ...], + type: {:list, :string} | {:list, :string} | :integer + }, + ... + ], + description: <<_::448>>, + key: :mrf_steal_emoji, + label: <<_::80>>, + related_policy: <<_::352>> + } + def config_description do + %{ + key: :mrf_steal_emoji, + related_policy: "Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy", + label: "MRF Emojis", + description: "Steals emojis from selected instances when it sees them.", + children: [ + %{ + key: :hosts, + type: {:list, :string}, + description: "List of hosts to steal emojis from", + suggestions: [""] + }, + %{ + key: :rejected_shortcodes, + type: {:list, :string}, + description: "Regex-list of shortcodes to reject", + suggestions: [""] + }, + %{ + key: :size_limit, + type: :integer, + description: "File size limit (in bytes), checked before an emoji is saved to the disk", + suggestions: ["100000"] + } + ] + } + end + @impl true def describe do {:ok, %{}} diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex index 86965d47b..11a36aca1 100644 --- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do require Logger - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp lookup_subchain(actor) do with matches <- Config.get([:mrf_subchain, :match_actor]), @@ -23,9 +23,7 @@ defp lookup_subchain(actor) do def filter(%{"actor" => actor} = message) do with {:ok, match, subchain} <- lookup_subchain(actor) do Logger.debug( - "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{ - inspect(subchain) - }" + "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{inspect(subchain)}" ) MRF.filter(subchain, message) diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 528093ac0..56ae654f2 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do alias Pleroma.User - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @moduledoc """ Apply policies based on user tags 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 65b371bf3..52fb02a84 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 @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do alias Pleroma.Config @moduledoc "Accept-list of users from specified instances" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy defp filter_by_list(object, []), do: {:ok, object} @@ -37,7 +37,7 @@ def filter(object), do: {:ok, object} def describe do mrf_user_allowlist = Config.get([:mrf_user_allowlist], []) - |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end) + |> Map.new(fn {k, v} -> {k, length(v)} end) {:ok, %{mrf_user_allowlist: mrf_user_allowlist}} end diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index ce559a239..602e10b44 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do @moduledoc "Filter messages which belong to certain activity vocabularies" - @behaviour Pleroma.Web.ActivityPub.MRF + @behaviour Pleroma.Web.ActivityPub.MRF.Policy @impl true def filter(%{"type" => "Undo", "object" => child_message} = message) do @@ -39,7 +39,7 @@ def filter(message), do: {:ok, message} @impl true def describe, - do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}} + do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Map.new()}} @impl true def config_description do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 1dce33f1a..187cd0cfd 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator @@ -102,7 +102,7 @@ def validate( %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, meta ) - when objtype in ~w[Question Answer Audio Video Event Article] do + when objtype in ~w[Question Answer Audio Video Event Article Note Page] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -115,14 +115,16 @@ def validate( end def validate(%{"type" => type} = object, meta) - when type in ~w[Event Question Audio Video Article] do + when type in ~w[Event Question Audio Video Article Note Page] do validator = case type do "Event" -> EventValidator "Question" -> QuestionValidator "Audio" -> AudioVideoValidator "Video" -> AudioVideoValidator - "Article" -> ArticleNoteValidator + "Article" -> ArticleNotePageValidator + "Note" -> ArticleNotePageValidator + "Page" -> ArticleNotePageValidator end with {:ok, object} <- @@ -174,6 +176,8 @@ def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do end end + def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} + def cast_and_apply(%{"type" => "ChatMessage"} = object) do ChatMessageValidator.cast_and_apply(object) end @@ -194,8 +198,8 @@ def cast_and_apply(%{"type" => "Event"} = object) do EventValidator.cast_and_apply(object) end - def cast_and_apply(%{"type" => "Article"} = object) do - ArticleNoteValidator.cast_and_apply(object) + def cast_and_apply(%{"type" => type} = object) when type in ~w[Article Note Page] do + ArticleNotePageValidator.cast_and_apply(object) end def cast_and_apply(o), do: {:error, {:validator_not_set, o}} @@ -209,6 +213,7 @@ def stringify_keys(%{__struct__: _} = object) do def stringify_keys(object) when is_map(object) do object + |> Enum.filter(fn {_, v} -> v != nil end) |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end) end diff --git a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex index b577a1044..7c3c8d0fa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do use Ecto.Schema alias Pleroma.Activity - alias Pleroma.EctoType.ActivityPub.ObjectValidators import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,12 +13,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end end def cast_data(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex index f885aabe4..fc482c9c0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex @@ -10,19 +10,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do require Pleroma.Constants - alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.User @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) field(:target) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) - field(:type) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) + + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end end def cast_and_validate(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex index 576341790..a7f2f6673 100644 --- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility @@ -19,13 +20,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) - field(:context, :string, autogenerate: {Utils, :generate_context_id, []}) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + + field(:context, :string) field(:published, ObjectValidators.DateTime) end @@ -36,6 +39,10 @@ def cast_and_validate(data) do end def cast_data(data) do + data = + data + |> fix() + %__MODULE__{} |> changeset(data) end @@ -43,11 +50,21 @@ def cast_data(data) do def changeset(struct, data) do struct |> cast(data, __schema__(:fields)) - |> fix_after_cast() end - def fix_after_cast(cng) do - cng + defp fix(data) do + data = + data + |> CommonFixes.fix_actor() + |> CommonFixes.fix_activity_addressing() + + with %Object{} = object <- Object.normalize(data["object"]) do + data + |> CommonFixes.fix_activity_context(object) + |> CommonFixes.fix_object_action_recipients(object) + else + _ -> data + end end defp validate_data(data_cng) do @@ -60,7 +77,7 @@ defp validate_data(data_cng) do |> validate_announcable() end - def validate_announcable(cng) do + defp validate_announcable(cng) do with actor when is_binary(actor) <- get_field(cng, :actor), object when is_binary(object) <- get_field(cng, :object), %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -68,7 +85,7 @@ def validate_announcable(cng) do false <- Visibility.is_public?(object) do same_actor = object.data["actor"] == actor.ap_id recipients = get_field(cng, :to) ++ get_field(cng, :cc) - local_public = Pleroma.Constants.as_local_public() + local_public = Utils.as_local_public() is_public = Enum.member?(recipients, Pleroma.Constants.as_public()) or @@ -91,7 +108,7 @@ def validate_announcable(cng) do end end - def validate_existing_announce(cng) do + defp validate_existing_announce(cng) do actor = get_field(cng, :actor) object = get_field(cng, :object) diff --git a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex index c9bd9e42d..4325e44f7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/answer_validator.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Ecto.Changeset @@ -14,15 +15,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do @derive Jason.Encoder embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - field(:type, :string) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + end + end + field(:name, :string) field(:inReplyTo, ObjectValidators.ObjectID) field(:attributedTo, ObjectValidators.ObjectID) + field(:context, :string) # TODO: Remove actor on objects field(:actor, ObjectValidators.ObjectID) @@ -46,6 +49,11 @@ def cast_data(data) do end def changeset(struct, data) do + data = + data + |> CommonFixes.fix_actor() + |> CommonFixes.fix_object_defaults() + struct |> cast(data, __schema__(:fields)) end diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex new file mode 100644 index 000000000..0aa249c4c --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex @@ -0,0 +1,97 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Ecto.Changeset + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + object_fields() + status_object_fields() + end + end + + field(:replies, {:array, ObjectValidators.ObjectID}, default: []) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data + defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"]) + defp fix_url(data), do: data + + defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data + defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag]) + defp fix_tag(data), do: Map.drop(data, ["tag"]) + + defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data) + when is_list(replies), + do: Map.put(data, "replies", replies) + + defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies), + do: Map.put(data, "replies", replies) + + defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies), + do: Map.drop(data, ["replies"]) + + defp fix_replies(data), do: data + + defp fix(data) do + data + |> CommonFixes.fix_actor() + |> CommonFixes.fix_object_defaults() + |> fix_url() + |> fix_tag() + |> fix_replies() + |> Transmogrifier.fix_emoji() + |> Transmogrifier.fix_content_map() + end + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, __schema__(:fields) -- [:attachment, :tag]) + |> cast_embed(:attachment) + |> cast_embed(:tag) + end + + defp validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["Article", "Note", "Page"]) + |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) + |> CommonValidations.validate_any_presence([:cc, :to]) + |> CommonValidations.validate_fields_match([:actor, :attributedTo]) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_host_match() + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex deleted file mode 100644 index 39ef6dc29..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex +++ /dev/null @@ -1,107 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes - alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations - alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator - alias Pleroma.Web.ActivityPub.Transmogrifier - - import Ecto.Changeset - - @primary_key false - @derive Jason.Encoder - - embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - embeds_many(:tag, TagValidator) - field(:type, :string) - - field(:name, :string) - field(:summary, :string) - field(:content, :string) - - field(:context, :string) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - # TODO: Remove actor on objects - field(:actor, ObjectValidators.ObjectID) - - field(:attributedTo, ObjectValidators.ObjectID) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - embeds_many(:attachment, AttachmentValidator) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - - field(:likes, {:array, ObjectValidators.ObjectID}, default: []) - field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) - end - - def cast_and_apply(data) do - data - |> cast_data - |> apply_action(:insert) - end - - def cast_and_validate(data) do - data - |> cast_data() - |> validate_data() - end - - def cast_data(data) do - data = fix(data) - - %__MODULE__{} - |> changeset(data) - end - - defp fix_url(%{"url" => url} = data) when is_map(url) do - Map.put(data, "url", url["href"]) - end - - defp fix_url(data), do: data - - defp fix(data) do - data - |> CommonFixes.fix_defaults() - |> CommonFixes.fix_attribution() - |> CommonFixes.fix_actor() - |> fix_url() - |> Transmogrifier.fix_emoji() - end - - def changeset(struct, data) do - data = fix(data) - - struct - |> cast(data, __schema__(:fields) -- [:attachment, :tag]) - |> cast_embed(:attachment) - |> cast_embed(:tag) - end - - defp validate_data(data_cng) do - data_cng - |> validate_inclusion(:type, ["Article", "Note"]) - |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) - |> CommonValidations.validate_any_presence([:cc, :to]) - |> CommonValidations.validate_fields_match([:actor, :attributedTo]) - |> CommonValidations.validate_actor_presence() - |> CommonValidations.validate_host_match() - end -end diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index 4a0d1473d..59fef42d6 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do field(:type, :string) field(:href, ObjectValidators.Uri) field(:mediaType, :string, default: "application/octet-stream") + field(:width, :integer) + field(:height, :integer) end end @@ -51,7 +53,7 @@ def url_changeset(struct, data) do data = fix_media_type(data) struct - |> cast(data, [:type, :href, :mediaType]) + |> cast(data, [:type, :href, :mediaType, :width, :height]) |> validate_inclusion(:type, ["Link"]) |> validate_required([:type, :href, :mediaType]) end @@ -59,19 +61,21 @@ def url_changeset(struct, data) do def fix_media_type(data) do data = Map.put_new(data, "mediaType", data["mimeType"]) - if MIME.valid?(data["mediaType"]) do + if is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] do data else Map.put(data, "mediaType", "application/octet-stream") end end - defp handle_href(href, mediaType) do + defp handle_href(href, mediaType, data) do [ %{ "href" => href, "type" => "Link", - "mediaType" => mediaType + "mediaType" => mediaType, + "width" => data["width"], + "height" => data["height"] } ] end @@ -79,10 +83,10 @@ defp handle_href(href, mediaType) do defp fix_url(data) do cond do is_binary(data["url"]) -> - Map.put(data, "url", handle_href(data["url"], data["mediaType"])) + Map.put(data, "url", handle_href(data["url"], data["mediaType"], data)) is_binary(data["href"]) and data["url"] == nil -> - Map.put(data, "url", handle_href(data["href"], data["mediaType"])) + Map.put(data, "url", handle_href(data["href"], data["mediaType"], data)) true -> data diff --git a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex index 27e14b16d..331ec9050 100644 --- a/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex @@ -5,11 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do use Ecto.Schema - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations - alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -18,38 +15,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do @derive Jason.Encoder embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - embeds_many(:tag, TagValidator) - field(:type, :string) - - field(:name, :string) - field(:summary, :string) - field(:content, :string) - - field(:context, :string) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - # TODO: Remove actor on objects - field(:actor, ObjectValidators.ObjectID) - - field(:attributedTo, ObjectValidators.ObjectID) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - embeds_many(:attachment, AttachmentValidator) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - - field(:likes, {:array, ObjectValidators.ObjectID}, default: []) - field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + object_fields() + status_object_fields() + end + end end def cast_and_apply(data) do @@ -119,9 +92,8 @@ defp fix_content(data), do: data defp fix(data) do data - |> CommonFixes.fix_defaults() - |> CommonFixes.fix_attribution() |> CommonFixes.fix_actor() + |> CommonFixes.fix_object_defaults() |> Transmogrifier.fix_emoji() |> fix_url() |> fix_content() diff --git a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex index 88948135f..400e5e278 100644 --- a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex @@ -5,20 +5,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do use Ecto.Schema - alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Ecto.Changeset - import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false + @derive Jason.Encoder 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) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end end def cast_data(data) do @@ -30,8 +31,8 @@ defp 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) + |> CommonValidations.validate_actor_presence() + |> CommonValidations.validate_actor_presence(field_name: :object) end def cast_and_validate(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex new file mode 100644 index 000000000..872f80ec3 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator + + # Activities and Objects, except (Create)ChatMessage + defmacro message_fields do + quote bind_quoted: binding() do + field(:type, :string) + field(:id, ObjectValidators.ObjectID, primary_key: true) + + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:bto, ObjectValidators.Recipients, default: []) + field(:bcc, ObjectValidators.Recipients, default: []) + end + end + + defmacro activity_fields do + quote bind_quoted: binding() do + field(:object, ObjectValidators.ObjectID) + field(:actor, ObjectValidators.ObjectID) + end + end + + # All objects except Answer and CHatMessage + defmacro object_fields do + quote bind_quoted: binding() do + field(:content, :string) + + field(:published, ObjectValidators.DateTime) + field(:emoji, ObjectValidators.Emoji, default: %{}) + embeds_many(:attachment, AttachmentValidator) + end + end + + # Basically objects that aren't ChatMessage and Answer + defmacro status_object_fields do + quote bind_quoted: binding() do + # TODO: Remove actor on objects + field(:actor, ObjectValidators.ObjectID) + field(:attributedTo, ObjectValidators.ObjectID) + + embeds_many(:tag, TagValidator) + + field(:name, :string) + field(:summary, :string) + + field(:context, :string) + # short identifier for PleromaFE to group statuses by context + field(:context_id, :integer) + + field(:sensitive, :boolean, default: false) + field(:replies_count, :integer, default: 0) + field(:like_count, :integer, default: 0) + field(:announcement_count, :integer, default: 0) + field(:inReplyTo, ObjectValidators.ObjectID) + field(:url, ObjectValidators.Uri) + + field(:likes, {:array, ObjectValidators.ObjectID}, default: []) + field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + end + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 5f2c633bc..9631013a7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -3,29 +3,76 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do + alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Object alias Pleroma.Object.Containment + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils - # based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults - def fix_defaults(data) do + def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do + {:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback) + + data = + Enum.reject(data, fn x -> + String.ends_with?(x, "/followers") and x != follower_collection + end) + + Map.put(message, field, data) + end + + def fix_object_defaults(data) do %{data: %{"id" => context}, id: context_id} = Utils.create_context(data["context"] || data["conversation"]) + %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"]) + data |> Map.put("context", context) |> Map.put("context_id", context_id) + |> cast_and_filter_recipients("to", follower_collection) + |> cast_and_filter_recipients("cc", follower_collection) + |> cast_and_filter_recipients("bto", follower_collection) + |> cast_and_filter_recipients("bcc", follower_collection) + |> Transmogrifier.fix_implicit_addressing(follower_collection) end - def fix_attribution(data) do - data - |> Map.put_new("actor", data["attributedTo"]) + def fix_activity_addressing(activity) do + %User{follower_address: follower_collection} = User.get_cached_by_ap_id(activity["actor"]) + + activity + |> cast_and_filter_recipients("to", follower_collection) + |> cast_and_filter_recipients("cc", follower_collection) + |> cast_and_filter_recipients("bto", follower_collection) + |> cast_and_filter_recipients("bcc", follower_collection) + |> Transmogrifier.fix_implicit_addressing(follower_collection) end def fix_actor(data) do - actor = Containment.get_actor(data) + actor = + data + |> Map.put_new("actor", data["attributedTo"]) + |> Containment.get_actor() data |> Map.put("actor", actor) |> Map.put("attributedTo", actor) end + + def fix_activity_context(data, %Object{data: %{"context" => object_context}}) do + data + |> Map.put("context", object_context) + end + + def fix_object_action_recipients(%{"actor" => actor} = data, %Object{data: %{"actor" => actor}}) do + to = ((data["to"] || []) -- [actor]) |> Enum.uniq() + + Map.put(data, "to", to) + end + + def fix_object_action_recipients(data, %Object{data: %{"actor" => actor}}) do + to = ((data["to"] || []) ++ [actor]) |> Enum.uniq() + + Map.put(data, "to", to) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index 940430588..be5074348 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -15,6 +15,7 @@ def validate_any_presence(cng, fields) do fields |> Enum.map(fn field -> get_field(cng, field) end) |> Enum.any?(fn + nil -> false [] -> false _ -> true end) diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex index 7a31a99bf..6551f64ca 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -17,11 +17,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do @primary_key false embedded_schema do + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + activity_fields() + end + end + field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:actor, ObjectValidators.ObjectID) field(:type, :string) field(:to, ObjectValidators.Recipients, default: []) - field(:object, ObjectValidators.ObjectID) end def cast_and_apply(data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex index e06e442f4..803b5d5a1 100644 --- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex @@ -10,20 +10,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:actor, ObjectValidators.ObjectID) - field(:type, :string) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:object, ObjectValidators.ObjectID) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + field(:expires_at, ObjectValidators.DateTime) # Should be moved to object, done for CommonAPI.Utils.make_context @@ -54,39 +58,37 @@ def changeset(struct, data) do |> cast(data, __schema__(:fields)) end - defp fix_context(data, meta) do - if object = meta[:object_data] do - Map.put_new(data, "context", object["context"]) - else - data - end - end + # CommonFixes.fix_activity_addressing adapted for Create specific behavior + defp fix_addressing(data, object) do + %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["actor"]) + + data + |> CommonFixes.cast_and_filter_recipients("to", follower_collection, object["to"]) + |> CommonFixes.cast_and_filter_recipients("cc", follower_collection, object["cc"]) + |> CommonFixes.cast_and_filter_recipients("bto", follower_collection, object["bto"]) + |> CommonFixes.cast_and_filter_recipients("bcc", follower_collection, object["bcc"]) + |> Transmogrifier.fix_implicit_addressing(follower_collection) + end + + def fix(data, meta) do + object = meta[:object_data] - defp fix_addressing(data, meta) do - if object = meta[:object_data] do - data - |> Map.put_new("to", object["to"] || []) - |> Map.put_new("cc", object["cc"] || []) - else - data - end - end - - defp fix(data, meta) do data - |> fix_context(meta) - |> fix_addressing(meta) |> CommonFixes.fix_actor() + |> Map.put_new("context", object["context"]) + |> fix_addressing(object) end defp validate_data(cng, meta) do + object = meta[:object_data] + cng - |> validate_required([:actor, :type, :object]) + |> validate_required([:actor, :type, :object, :to, :cc]) |> validate_inclusion(:type, ["Create"]) |> CommonValidations.validate_actor_presence() - |> CommonValidations.validate_any_presence([:to, :cc]) - |> validate_actors_match(meta) - |> validate_context_match(meta) + |> validate_actors_match(object) + |> validate_context_match(object) + |> validate_addressing_match(object) |> validate_object_nonexistence() |> validate_object_containment() end @@ -118,8 +120,8 @@ def validate_object_nonexistence(cng) do end) end - def validate_actors_match(cng, meta) do - attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"] + def validate_actors_match(cng, object) do + attributed_to = object["attributedTo"] || object["actor"] cng |> validate_change(:actor, fn :actor, actor -> @@ -131,7 +133,7 @@ def validate_actors_match(cng, meta) do end) end - def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do + def validate_context_match(cng, %{"context" => object_context}) do cng |> validate_change(:context, fn :context, context -> if context == object_context do @@ -142,5 +144,18 @@ def validate_context_match(cng, %{object_data: %{"context" => object_context}}) end) end - def validate_context_match(cng, _), do: cng + def validate_addressing_match(cng, object) do + [:to, :cc, :bcc, :bto] + |> Enum.reduce(cng, fn field, cng -> + object_data = object[to_string(field)] + + validate_change(cng, field, fn field, data -> + if data == object_data do + [] + else + [{field, "field doesn't match with object (#{inspect(object_data)})"}] + end + end) + end) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex deleted file mode 100644 index a85a0298c..000000000 --- a/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex +++ /dev/null @@ -1,29 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do - use Ecto.Schema - - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator - - import Ecto.Changeset - - @primary_key false - - embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:actor, ObjectValidators.ObjectID) - field(:type, :string) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - embeds_one(:object, NoteValidator) - end - - def cast_data(data) do - cast(%__MODULE__{}, data, __schema__(:fields)) - end -end diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index 7da67bf16..f0c99356e 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do alias Pleroma.Activity alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,13 +15,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do @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: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + field(:deleted_activity_id, ObjectValidators.ObjectID) - field(:object, ObjectValidators.ObjectID) end def cast_data(data) do @@ -57,7 +60,7 @@ defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Delete"]) - |> validate_actor_presence() + |> validate_delete_actor(:actor) |> validate_modification_rights() |> validate_object_or_user_presence(allowed_types: @deletable_types) |> add_deleted_activity_id() @@ -72,4 +75,13 @@ def cast_and_validate(data) do |> cast_data |> validate_data end + + defp validate_delete_actor(cng, field_name) do + validate_change(cng, field_name, fn field_name, actor -> + case User.get_cached_by_ap_id(actor) do + %User{} -> [] + _ -> [{field_name, "can't find user"}] + end + end) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index ec7566515..9eaaf8319 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do use Ecto.Schema - alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,14 +14,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + field(:context, :string) field(:content, :string) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) end def cast_and_validate(data) do @@ -31,6 +33,10 @@ def cast_and_validate(data) do end def cast_data(data) do + data = + data + |> fix() + %__MODULE__{} |> changeset(data) end @@ -38,28 +44,24 @@ def cast_data(data) do def changeset(struct, data) do struct |> cast(data, __schema__(:fields)) - |> fix_after_cast() end - def fix_after_cast(cng) do - cng - |> fix_context() - end + defp fix(data) do + data = + data + |> CommonFixes.fix_actor() + |> CommonFixes.fix_activity_addressing() - def fix_context(cng) do - object = get_field(cng, :object) - - with nil <- get_field(cng, :context), - %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do - cng - |> put_change(:context, context) + with %Object{} = object <- Object.normalize(data["object"]) do + data + |> CommonFixes.fix_activity_context(object) + |> CommonFixes.fix_object_action_recipients(object) else - _ -> - cng + _ -> data end end - def validate_emoji(cng) do + defp validate_emoji(cng) do content = get_field(cng, :content) if Pleroma.Emoji.is_unicode_emoji?(content) do diff --git a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex index d42458ef5..34a3031c3 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -5,11 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do use Ecto.Schema - alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations - alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -19,38 +16,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do # Extends from NoteValidator embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - embeds_many(:tag, TagValidator) - field(:type, :string) - - field(:name, :string) - field(:summary, :string) - field(:content, :string) - - field(:context, :string) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - # TODO: Remove actor on objects - field(:actor, ObjectValidators.ObjectID) - - field(:attributedTo, ObjectValidators.ObjectID) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - embeds_many(:attachment, AttachmentValidator) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - - field(:likes, {:array, ObjectValidators.ObjectID}, default: []) - field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + object_fields() + status_object_fields() + end + end end def cast_and_apply(data) do @@ -72,8 +45,8 @@ def cast_data(data) do defp fix(data) do data - |> CommonFixes.fix_defaults() - |> CommonFixes.fix_attribution() + |> CommonFixes.fix_actor() + |> CommonFixes.fix_object_defaults() |> Transmogrifier.fix_emoji() end diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex index 239cee5e7..c061ebba9 100644 --- a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -5,20 +5,20 @@ 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) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + field(:state, :string, default: "pending") end diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex index 509da507b..35e000d72 100644 --- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do use Ecto.Schema - alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.Utils import Ecto.Changeset @@ -15,13 +15,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end + field(:context, :string) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) end def cast_and_validate(data) do @@ -31,6 +33,10 @@ def cast_and_validate(data) do end def cast_data(data) do + data = + data + |> fix() + %__MODULE__{} |> changeset(data) end @@ -38,41 +44,20 @@ def cast_data(data) do def changeset(struct, data) do struct |> cast(data, __schema__(:fields)) - |> fix_after_cast() end - def fix_after_cast(cng) do - cng - |> fix_recipients() - |> fix_context() - end + defp fix(data) do + data = + data + |> CommonFixes.fix_actor() + |> CommonFixes.fix_activity_addressing() - def fix_context(cng) do - object = get_field(cng, :object) - - with nil <- get_field(cng, :context), - %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do - cng - |> put_change(:context, context) + with %Object{} = object <- Object.normalize(data["object"]) do + data + |> CommonFixes.fix_activity_context(object) + |> CommonFixes.fix_object_action_recipients(object) else - _ -> - cng - end - end - - def fix_recipients(cng) do - to = get_field(cng, :to) - cc = get_field(cng, :cc) - object = get_field(cng, :object) - - with {[], []} <- {to, cc}, - %Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object), - {:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do - cng - |> put_change(:to, [actor]) - else - _ -> - cng + _ -> data end end @@ -85,7 +70,7 @@ defp validate_data(data_cng) do |> validate_existing_like() end - def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do + defp validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do cng |> add_error(:actor, "already liked this object") @@ -95,5 +80,5 @@ def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do end end - def validate_existing_like(cng), do: cng + defp validate_existing_like(cng), do: cng end diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 7012e2e1d..bdddfdaeb 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -6,11 +6,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator - alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator alias Pleroma.Web.ActivityPub.Transmogrifier import Ecto.Changeset @@ -20,35 +18,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do # Extends from NoteValidator embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) - field(:bto, ObjectValidators.Recipients, default: []) - field(:bcc, ObjectValidators.Recipients, default: []) - embeds_many(:tag, TagValidator) - field(:type, :string) - field(:content, :string) - field(:context, :string) - - # TODO: Remove actor on objects - field(:actor, ObjectValidators.ObjectID) - - field(:attributedTo, ObjectValidators.ObjectID) - field(:summary, :string) - field(:published, ObjectValidators.DateTime) - field(:emoji, ObjectValidators.Emoji, default: %{}) - field(:sensitive, :boolean, default: false) - embeds_many(:attachment, AttachmentValidator) - field(:replies_count, :integer, default: 0) - field(:like_count, :integer, default: 0) - field(:announcement_count, :integer, default: 0) - field(:inReplyTo, ObjectValidators.ObjectID) - field(:url, ObjectValidators.Uri) - # short identifier for PleromaFE to group statuses by context - field(:context_id, :integer) - - field(:likes, {:array, ObjectValidators.ObjectID}, default: []) - field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + object_fields() + status_object_fields() + end + end field(:closed, ObjectValidators.DateTime) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) @@ -83,8 +60,8 @@ defp fix_closed(data) do defp fix(data) do data - |> CommonFixes.fix_defaults() - |> CommonFixes.fix_attribution() + |> CommonFixes.fix_actor() + |> CommonFixes.fix_object_defaults() |> Transmogrifier.fix_emoji() |> fix_closed() end diff --git a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex index e8af60ffa..703643e3f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do use Ecto.Schema alias Pleroma.Activity - alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.User import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @@ -14,12 +14,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) - field(:object, ObjectValidators.ObjectID) - field(:actor, ObjectValidators.ObjectID) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + activity_fields() + end + end end def cast_and_validate(data) do @@ -42,7 +43,7 @@ defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Undo"]) |> validate_required([:id, :type, :object, :actor, :to, :cc]) - |> validate_actor_presence() + |> validate_undo_actor(:actor) |> validate_object_presence() |> validate_undo_rights() end @@ -59,4 +60,13 @@ def validate_undo_rights(cng) do _ -> cng end end + + defp validate_undo_actor(cng, field_name) do + validate_change(cng, field_name, fn field_name, actor -> + case User.get_cached_by_ap_id(actor) do + %User{} -> [] + _ -> [{field_name, "can't find user"}] + end + end) + end end diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex index 6bb1dc7fa..a1fae47f5 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -13,11 +13,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do @primary_key false embedded_schema do - field(:id, ObjectValidators.ObjectID, primary_key: true) - field(:type, :string) + quote do + unquote do + import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields + message_fields() + end + end + field(:actor, ObjectValidators.ObjectID) - field(:to, ObjectValidators.Recipients, default: []) - field(:cc, ObjectValidators.Recipients, default: []) # In this case, we save the full object in this activity instead of just a # reference, so we can always see what was actually changed by this. field(:object, :map) diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index a0f2e0312..0d6e8aad2 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -15,19 +15,19 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator - @side_effects Config.get([:pipeline, :side_effects], SideEffects) - @federator Config.get([:pipeline, :federator], Federator) - @object_validator Config.get([:pipeline, :object_validator], ObjectValidator) - @mrf Config.get([:pipeline, :mrf], MRF) - @activity_pub Config.get([:pipeline, :activity_pub], ActivityPub) - @config Config.get([:pipeline, :config], Config) + defp side_effects, do: Config.get([:pipeline, :side_effects], SideEffects) + defp federator, do: Config.get([:pipeline, :federator], Federator) + defp object_validator, do: Config.get([:pipeline, :object_validator], ObjectValidator) + defp mrf, do: Config.get([:pipeline, :mrf], MRF) + defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub) + defp config, do: Config.get([:pipeline, :config], Config) @spec common_pipeline(map(), keyword()) :: {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} def common_pipeline(object, meta) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do {:ok, {:ok, activity, meta}} -> - @side_effects.handle_after_transaction(meta) + side_effects().handle_after_transaction(meta) {:ok, activity, meta} {:ok, value} -> @@ -44,10 +44,10 @@ def common_pipeline(object, meta) do def do_common_pipeline(%{__struct__: _}, _meta), do: {:error, :is_struct} def do_common_pipeline(message, meta) do - with {_, {:ok, message, meta}} <- {:validate, @object_validator.validate(message, meta)}, - {_, {:ok, message, meta}} <- {:mrf, @mrf.pipeline_filter(message, meta)}, - {_, {:ok, message, meta}} <- {:persist, @activity_pub.persist(message, meta)}, - {_, {:ok, message, meta}} <- {:side_effects, @side_effects.handle(message, meta)}, + with {_, {:ok, message, meta}} <- {:validate, object_validator().validate(message, meta)}, + {_, {:ok, message, meta}} <- {:mrf, mrf().pipeline_filter(message, meta)}, + {_, {:ok, message, meta}} <- {:persist, activity_pub().persist(message, meta)}, + {_, {:ok, message, meta}} <- {:side_effects, side_effects().handle(message, meta)}, {_, {:ok, _}} <- {:federation, maybe_federate(message, meta)} do {:ok, message, meta} else @@ -60,7 +60,7 @@ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated} defp maybe_federate(%Activity{} = activity, meta) do with {:ok, local} <- Keyword.fetch(meta, :local) do - do_not_federate = meta[:do_not_federate] || !@config.get([:instance, :federating]) + do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating]) if !do_not_federate and local and not Visibility.is_local_public?(activity) do activity = @@ -70,7 +70,7 @@ defp maybe_federate(%Activity{} = activity, meta) do activity end - @federator.publish(activity) + federator().publish(activity) {:ok, :federated} else {:ok, :not_federated} diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index b12b2fc24..ed99079e2 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -63,18 +63,17 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa date: date }) - with {:ok, %{status: code}} when code in 200..299 <- - result = - HTTP.post( - inbox, - json, - [ - {"Content-Type", "application/activity+json"}, - {"Date", date}, - {"signature", signature}, - {"digest", digest} - ] - ) do + with {:ok, %{status: code}} = result when code in 200..299 <- + HTTP.post( + inbox, + json, + [ + {"Content-Type", "application/activity+json"}, + {"Date", date}, + {"signature", signature}, + {"digest", digest} + ] + ) do if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do Instances.set_reachable(inbox) end @@ -112,6 +111,7 @@ defp should_federate?(inbox, public) do quarantined_instances = Config.get([:instance, :quarantined_instances], []) + |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples() |> Pleroma.Web.ActivityPub.MRF.subdomains_regex() !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host) @@ -272,7 +272,7 @@ def gather_webfinger_links(%User{} = user) do }, %{ "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" + "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}" } ] end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 5fe143c2b..d55a4b340 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -10,7 +10,6 @@ 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 @@ -24,15 +23,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Push alias Pleroma.Web.Streamer + alias Pleroma.Workers.PollWorker require Logger @cachex Pleroma.Config.get([:cachex, :provider], Cachex) - @ap_streamer Pleroma.Config.get([:side_effects, :ap_streamer], ActivityPub) @logger Pleroma.Config.get([:side_effects, :logger], Logger) @behaviour Pleroma.Web.ActivityPub.SideEffects.Handling + defp ap_streamer, do: Pleroma.Config.get([:side_effects, :ap_streamer], ActivityPub) + @impl true def handle(object, meta \\ []) @@ -194,15 +195,28 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do # - Set up notifications @impl true def handle(%{data: %{"type" => "Create"}} = activity, meta) do - with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta), + with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta), %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do {:ok, notifications} = Notification.create_notifications(activity, do_send: false) {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object) - if in_reply_to = object.data["inReplyTo"] && object.data["type"] != "Answer" do + if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do Object.increase_replies_count(in_reply_to) end + reply_depth = (meta[:depth] || 0) + 1 + + # FIXME: Force inReplyTo to replies + if Pleroma.Web.Federator.allowed_thread_distance?(reply_depth) and + object.data["replies"] != nil do + for reply_id <- object.data["replies"] do + Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{ + "id" => reply_id, + "depth" => reply_depth + }) + end + end + ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn -> Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) end) @@ -211,6 +225,8 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do meta |> add_notifications(notifications) + ap_streamer().stream_out(activity) + {:ok, activity, meta} else e -> Repo.rollback(e) @@ -231,9 +247,7 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do if !User.is_internal_user?(user) do Notification.create_notifications(object) - object - |> Topics.get_activity_topics() - |> Streamer.stream(object) + ap_streamer().stream_out(object) end {:ok, object, meta} @@ -289,8 +303,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, MessageReference.delete_for_object(deleted_object) - @ap_streamer.stream_out(object) - @ap_streamer.stream_out_participations(deleted_object, user) + ap_streamer().stream_out(object) + ap_streamer().stream_out_participations(deleted_object, user) :ok else {:actor, _} -> @@ -375,7 +389,7 @@ def handle(object, meta) do {:ok, object, meta} end - def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do + def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do actor = User.get_cached_by_ap_id(object.data["actor"]) recipient = User.get_cached_by_ap_id(hd(object.data["to"])) @@ -410,7 +424,14 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do end end - def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do + def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + PollWorker.schedule_poll_end(activity) + {:ok, object, meta} + end + end + + def handle_object_creation(%{"type" => "Answer"} = object_map, _activity, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do Object.increase_vote_count( object.data["inReplyTo"], @@ -422,15 +443,15 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do end end - def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Video Question Event Article] do + def handle_object_creation(%{"type" => objtype} = object, _activity, meta) + when objtype in ~w[Audio Video Event Article Note Page] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do {:ok, object, meta} end end # Nothing to do - def handle_object_creation(object, meta) do + def handle_object_creation(object, _activity, meta) do {:ok, object, meta} end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d27d0bed4..142af1a13 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -43,7 +43,6 @@ def fix_object(object, options \\ []) do |> fix_content_map() |> fix_addressing() |> fix_summary() - |> fix_type(options) end def fix_summary(%{"summary" => nil} = object) do @@ -72,17 +71,21 @@ def fix_addressing_list(map, field) do end end - def fix_explicit_addressing( - %{"to" => to, "cc" => cc} = object, - explicit_mentions, - follower_collection - ) do - explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end) + # if directMessage flag is set to true, leave the addressing alone + def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection), + do: object + def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do + explicit_mentions = + Utils.determine_explicit_mentions(object) ++ + [Pleroma.Constants.as_public(), follower_collection] + + explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end) explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end) final_cc = (cc ++ explicit_cc) + |> Enum.filter(& &1) |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.uniq() @@ -91,29 +94,6 @@ def fix_explicit_addressing( |> Map.put("cc", final_cc) end - def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object - - # if directMessage flag is set to true, leave the addressing alone - def fix_explicit_addressing(%{"directMessage" => true} = object), do: object - - def fix_explicit_addressing(object) do - explicit_mentions = Utils.determine_explicit_mentions(object) - - %User{follower_address: follower_collection} = - object - |> Containment.get_actor() - |> User.get_cached_by_ap_id() - - explicit_mentions = - explicit_mentions ++ - [ - Pleroma.Constants.as_public(), - follower_collection - ] - - fix_explicit_addressing(object, explicit_mentions, follower_collection) - end - # if as:Public is addressed, then make sure the followers collection is also addressed # so that the activities will be delivered to local users. def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do @@ -137,19 +117,19 @@ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collec end end - def fix_implicit_addressing(object, _), do: object - def fix_addressing(object) do - {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"]) - followers_collection = User.ap_followers(user) + {:ok, %User{follower_address: follower_collection}} = + object + |> Containment.get_actor() + |> User.get_or_fetch_by_ap_id() object |> fix_addressing_list("to") |> fix_addressing_list("cc") |> fix_addressing_list("bto") |> fix_addressing_list("bcc") - |> fix_explicit_addressing() - |> fix_implicit_addressing(followers_collection) + |> fix_explicit_addressing(follower_collection) + |> fix_implicit_addressing(follower_collection) end def fix_actor(%{"attributedTo" => actor} = object) do @@ -223,10 +203,17 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm media_type = cond do - is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"] - MIME.valid?(data["mediaType"]) -> data["mediaType"] - MIME.valid?(data["mimeType"]) -> data["mimeType"] - true -> nil + is_map(url) && MIME.extensions(url["mediaType"]) != [] -> + url["mediaType"] + + is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] -> + data["mediaType"] + + is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] -> + data["mimeType"] + + true -> + nil end href = @@ -244,6 +231,8 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm "type" => Map.get(url || %{}, "type", "Link") } |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("width", (url || %{})["width"] || data["width"]) + |> Maps.put_if_present("height", (url || %{})["height"] || data["height"]) %{ "url" => [attachment_url], @@ -340,19 +329,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do def fix_content_map(object), do: object - def fix_type(object, options \\ []) + defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options) + when is_binary(reply_id) do + options = Keyword.put(options, :fetch, true) - def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) - when is_binary(reply_id) do - with true <- Federator.allowed_thread_distance?(options[:depth]), - {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do + with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do Map.put(object, "type", "Answer") else _ -> object end end - def fix_type(object, _), do: object + defp fix_type(object, _options), do: object # Reduce the object list to find the reported user. defp get_reported(objects) do @@ -365,29 +353,6 @@ defp get_reported(objects) do end) end - # Compatibility wrapper for Mastodon votes - defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do - handle_incoming(data) - end - - defp handle_create(%{"object" => object} = data, user) do - %{ - to: data["to"], - object: object, - actor: user, - context: object["context"], - local: false, - published: data["published"], - additional: - Map.take(data, [ - "cc", - "directMessage", - "id" - ]) - } - |> ActivityPub.create() - end - def handle_incoming(data, options \\ []) # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them @@ -419,44 +384,6 @@ def handle_incoming(%{"id" => ""}, _options), do: :error def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8, do: :error - # TODO: validate those with a Ecto scheme - # - tags - # - emoji - def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, - options - ) - when objtype in ~w{Note Page} do - actor = Containment.get_actor(data) - - with nil <- Activity.get_create_by_object_ap_id(object["id"]), - {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do - data = - data - |> Map.put("object", fix_object(object, options)) - |> Map.put("actor", actor) - |> fix_addressing() - - with {:ok, created_activity} <- handle_create(data, user) do - reply_depth = (options[:depth] || 0) + 1 - - if Federator.allowed_thread_distance?(reply_depth) do - for reply_id <- replies(object) do - Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{ - "id" => reply_id, - "depth" => reply_depth - }) - end - end - - {:ok, created_activity} - end - else - %Activity{} = activity -> {:ok, activity} - _e -> :error - end - end - def handle_incoming( %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data, options @@ -518,14 +445,23 @@ def handle_incoming( def handle_incoming( %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, - _options + options ) - when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do - data = Map.put(data, "object", strip_internal_fields(data["object"])) + when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do + fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) + + object = + data["object"] + |> strip_internal_fields() + |> fix_type(fetch_options) + |> fix_in_reply_to(fetch_options) + + data = Map.put(data, "object", object) + options = Keyword.put(options, :local, false) with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), nil <- Activity.get_create_by_object_ap_id(obj_id), - {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do {:ok, activity} else %Activity{} = activity -> {:ok, activity} @@ -949,7 +885,7 @@ def prepare_attachments(object) do object |> Map.get("attachment", []) |> Enum.map(fn data -> - [%{"mediaType" => media_type, "href" => href} | _] = data["url"] + [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"] %{ "url" => href, @@ -957,6 +893,9 @@ def prepare_attachments(object) do "name" => data["name"], "type" => "Document" } + |> Maps.put_if_present("width", url["width"]) + |> Maps.put_if_present("height", url["height"]) + |> Maps.put_if_present("blurhash", data["blurhash"]) end) Map.put(object, "attachment", attachments) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index a4dc469dc..c1f6b2b49 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -12,7 +12,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.AdminAPI.AccountView @@ -38,6 +37,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) + def as_local_public, do: Endpoint.url() <> "/#Public" + # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. def get_ap_id(%{"id" => id} = _), do: id @@ -96,8 +97,11 @@ def maybe_splice_recipient(ap_id, params) do !label_in_collection?(ap_id, params["cc"]) if need_splice? do - cc_list = extract_list(params["cc"]) - Map.put(params, "cc", [ap_id | cc_list]) + cc = [ap_id | extract_list(params["cc"])] + + params + |> Map.put("cc", cc) + |> Maps.safe_put_in(["object", "cc"], cc) else params end @@ -107,7 +111,7 @@ def make_json_ld_header do %{ "@context" => [ "https://www.w3.org/ns/activitystreams", - "#{Web.base_url()}/schemas/litepub-0.1.jsonld", + "#{Endpoint.url()}/schemas/litepub-0.1.jsonld", %{ "@language" => "und" } @@ -132,7 +136,7 @@ def generate_object_id do end def generate_id(type) do - "#{Web.base_url()}/#{type}/#{UUID.generate()}" + "#{Endpoint.url()}/#{type}/#{UUID.generate()}" end def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do @@ -442,7 +446,7 @@ def update_follow_state_for_all( |> Activity.Queries.by_type() |> Activity.Queries.by_actor(actor) |> Activity.Queries.by_object_id(object) - |> where(fragment("data->>'state' = 'pending'")) + |> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'")) |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) |> Repo.update_all([]) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 462f3b4a7..344da19d3 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -261,7 +261,8 @@ def render("featured.json", %{ %{ "id" => featured_address, "type" => "OrderedCollection", - "orderedItems" => objects + "orderedItems" => objects, + "totalItems" => length(objects) } |> Map.merge(Utils.make_json_ld_header()) end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 00234c0b0..986fa3a08 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -20,14 +20,14 @@ def is_public?(%{"directMessage" => true}), do: false def is_public?(data) do Utils.label_in_message?(Pleroma.Constants.as_public(), data) or - Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) + Utils.label_in_message?(Utils.as_local_public(), data) end def is_local_public?(%Object{data: data}), do: is_local_public?(data) def is_local_public?(%Activity{data: data}), do: is_local_public?(data) def is_local_public?(data) do - Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) and + Utils.label_in_message?(Utils.as_local_public(), data) and not Utils.label_in_message?(Pleroma.Constants.as_public(), data) end @@ -57,6 +57,7 @@ def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(_), do: false @spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean() + def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%Object{data: %{"actor" => ap_id}}, %User{ap_id: ap_id}), do: true def visible_for_user?(nil, _), do: false @@ -127,7 +128,7 @@ def get_visibility(object) do Pleroma.Constants.as_public() in cc -> "unlisted" - Pleroma.Constants.as_local_public() in to -> + Utils.as_local_public() in to -> "local" # this should use the sql for the object's activity 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 839ac1a8d..50aa294f0 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["admin:read:statuses"]} - when action in [:list_user_statuses, :list_instance_statuses] + when action in [:list_user_statuses] ) plug( @@ -81,24 +81,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do action_fallback(AdminAPI.FallbackController) - def list_instance_statuses(conn, %{"instance" => instance} = params) do - with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true - {page, page_size} = page_params(params) - - result = - ActivityPub.fetch_statuses(nil, %{ - instance: instance, - limit: page_size, - offset: (page - 1) * page_size, - exclude_reblogs: not with_reblogs, - total: true - }) - - conn - |> put_view(AdminAPI.StatusView) - |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity}) - end - 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 diff --git a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex index 722f51bd2..442e6a5a0 100644 --- a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex @@ -35,6 +35,12 @@ def install(%{body_params: params} = conn, _params) do end defp installed do - File.ls!(Pleroma.Frontend.dir()) + frontend_directory = Pleroma.Frontend.dir() + + if File.exists?(frontend_directory) do + File.ls!(frontend_directory) + else + [] + end end end diff --git a/lib/pleroma/web/admin_api/controllers/instance_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_controller.ex new file mode 100644 index 000000000..00857983f --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/instance_controller.ex @@ -0,0 +1,63 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.InstanceController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 3] + + alias Pleroma.Instances.Instance + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.AdminAPI + alias Pleroma.Web.Plugs.OAuthScopesPlug + + require Logger + + @default_page_size 50 + + plug( + OAuthScopesPlug, + %{scopes: ["admin:read:statuses"]} + when action in [:list_statuses] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["admin:write:accounts", "admin:write:statuses"]} + when action in [:delete] + ) + + action_fallback(AdminAPI.FallbackController) + + def list_statuses(conn, %{"instance" => instance} = params) do + with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true + {page, page_size} = page_params(params) + + result = + ActivityPub.fetch_statuses(nil, %{ + instance: instance, + limit: page_size, + offset: (page - 1) * page_size, + exclude_reblogs: not with_reblogs, + total: true + }) + + conn + |> put_view(AdminAPI.StatusView) + |> render("index.json", %{total: result[:total], activities: result[:items], as: :activity}) + end + + def delete(conn, %{"instance" => instance}) do + with {:ok, _job} <- Instance.delete_users_and_activities(instance) do + json(conn, instance) + end + end + + defp page_params(params) do + { + fetch_integer_param(params, "page", 1), + fetch_integer_param(params, "page_size", @default_page_size) + } + end +end diff --git a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex index 005fe67e2..51b17d392 100644 --- a/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/o_auth_app_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do require Logger plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.AppView) plug( OAuthScopesPlug, diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex index d3e4c18a3..50208a8b7 100644 --- a/lib/pleroma/web/admin_api/controllers/user_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex @@ -35,7 +35,9 @@ defmodule Pleroma.Web.AdminAPI.UserController do :toggle_activation, :activate, :deactivate, - :approve + :approve, + :suggest, + :unsuggest ] ) @@ -45,8 +47,6 @@ defmodule Pleroma.Web.AdminAPI.UserController do when action in [:follow, :unfollow] ) - plug(:put_view, Pleroma.Web.AdminAPI.AccountView) - action_fallback(AdminAPI.FallbackController) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation @@ -241,6 +241,32 @@ def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = c render(conn, "index.json", users: updated_users) end + def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do + users = Enum.map(nicknames, &User.get_cached_by_nickname/1) + {:ok, updated_users} = User.set_suggestion(users, true) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "add_suggestion" + }) + + render(conn, "index.json", users: updated_users) + end + + def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do + users = Enum.map(nicknames, &User.get_cached_by_nickname/1) + {:ok, updated_users} = User.set_suggestion(users, false) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "remove_suggestion" + }) + + render(conn, "index.json", users: updated_users) + end + def index(conn, params) do {page, page_size} = page_params(params) filters = maybe_parse_filters(params[:filters]) diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex index 259068f04..345bc1e87 100644 --- a/lib/pleroma/web/admin_api/report.ex +++ b/lib/pleroma/web/admin_api/report.ex @@ -13,7 +13,9 @@ def extract_report_info( account = User.get_cached_by_ap_id(account_ap_id) statuses = - Enum.map(status_ap_ids, fn + status_ap_ids + |> Enum.reject(&is_nil(&1)) + |> Enum.map(fn act when is_map(act) -> Activity.get_by_ap_id_with_object(act["id"]) act when is_binary(act) -> Activity.get_by_ap_id_with_object(act) end) diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index eeeebdf4e..da38fab56 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -10,12 +10,6 @@ defmodule Pleroma.Web.AdminAPI.Search do @page_size 50 - defmacro not_empty_string(string) do - quote do - is_binary(unquote(string)) and unquote(string) != "" - end - end - @spec user(map()) :: {:ok, [User.t()], pos_integer()} def user(params \\ %{}) do query = @@ -23,7 +17,7 @@ def user(params \\ %{}) do |> Map.drop([:page, :page_size]) |> Map.put(:invisible, false) |> User.Query.build() - |> order_by([u], u.nickname) + |> order_by(desc: :id) paginated_query = User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index e053a9b67..2f1f7e627 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do alias Pleroma.User alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI alias Pleroma.Web.MediaProxy @@ -79,9 +80,11 @@ def render("show.json", %{user: user}) do "tags" => user.tags || [], "is_confirmed" => user.is_confirmed, "is_approved" => user.is_approved, + "is_suggested" => user.is_suggested, "url" => user.uri || user.ap_id, "registration_reason" => user.registration_reason, - "actor_type" => user.actor_type + "actor_type" => user.actor_type, + "created_at" => CommonAPI.Utils.to_masto_date(user.inserted_at) } end diff --git a/lib/pleroma/web/admin_api/views/o_auth_app_view.ex b/lib/pleroma/web/admin_api/views/o_auth_app_view.ex new file mode 100644 index 000000000..af046f343 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/o_auth_app_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.OAuthAppView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.AppView.render(view, opts) +end diff --git a/lib/pleroma/web/admin_api/views/user_view.ex b/lib/pleroma/web/admin_api/views/user_view.ex new file mode 100644 index 000000000..e91265ffe --- /dev/null +++ b/lib/pleroma/web/admin_api/views/user_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.UserView do + use Pleroma.Web, :view + alias Pleroma.Web.AdminAPI + + def render(view, opts), do: AdminAPI.AccountView.render(view, opts) +end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 54e5ebc76..8cd2e824d 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -226,6 +226,12 @@ def follow_operation do type: :boolean, description: "Receive this account's reblogs in home timeline? Defaults to true.", default: true + }, + notify: %Schema{ + type: :boolean, + description: + "Receive notifications for all statuses posted by the account? Defaults to false.", + default: false } } }, @@ -328,6 +334,29 @@ def unblock_operation do } end + def note_operation do + %Operation{ + tags: ["Account actions"], + summary: "Set a private note about a user.", + operationId: "AccountController.note", + security: [%{"oAuth" => ["follow", "write:accounts"]}], + requestBody: request_body("Parameters", note_request()), + description: "Create a note for the given account.", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, + Operation.parameter( + :comment, + :query, + %Schema{type: :string}, + "Account note body" + ) + ], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + def follow_by_uri_operation do %Operation{ tags: ["Account actions"], @@ -685,9 +714,11 @@ defp array_of_relationships do "blocked_by" => true, "muting" => false, "muting_notifications" => false, + "note" => "", "requested" => false, "domain_blocking" => false, "subscribing" => false, + "notifying" => false, "endorsed" => true }, %{ @@ -699,9 +730,11 @@ defp array_of_relationships do "blocked_by" => true, "muting" => true, "muting_notifications" => false, + "note" => "", "requested" => true, "domain_blocking" => false, "subscribing" => false, + "notifying" => false, "endorsed" => false }, %{ @@ -713,9 +746,11 @@ defp array_of_relationships do "blocked_by" => false, "muting" => true, "muting_notifications" => false, + "note" => "", "requested" => false, "domain_blocking" => true, "subscribing" => true, + "notifying" => true, "endorsed" => false } ] @@ -760,6 +795,23 @@ defp mute_request do } end + defp note_request do + %Schema{ + title: "AccountNoteRequest", + description: "POST body for adding a note for an account", + type: :object, + properties: %{ + comment: %Schema{ + type: :string, + description: "Account note body" + } + }, + example: %{ + "comment" => "Example note" + } + } + end + defp array_of_lists do %Schema{ title: "ArrayOfLists", diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex index c9d0bfd7c..57fb1ad65 100644 --- a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex @@ -216,7 +216,71 @@ def approve_operation do request_body( "Parameters", %Schema{ - description: "POST body for deleting multiple users", + description: "POST body for approving multiple users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{user: %Schema{type: :array, items: user()}} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def suggest_operation do + %Operation{ + tags: ["User administration"], + summary: "Suggest multiple users", + operationId: "AdminAPI.UserController.suggest", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for adding multiple suggested users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{user: %Schema{type: :array, items: user()}} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def unsuggest_operation do + %Operation{ + tags: ["User administration"], + summary: "Unsuggest multiple users", + operationId: "AdminAPI.UserController.unsuggest", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for removing multiple suggested users", type: :object, properties: %{ nicknames: %Schema{ diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index 85aa14869..451b6510f 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -24,6 +24,7 @@ def create_operation do requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ 200 => Operation.response("Media", "application/json", Attachment), + 400 => Operation.response("Media", "application/json", ApiError), 401 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError) } @@ -105,6 +106,7 @@ def show_operation do responses: %{ 200 => Operation.response("Media", "application/json", Attachment), 401 => Operation.response("Media", "application/json", ApiError), + 403 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError) } } @@ -120,6 +122,7 @@ def create2_operation do requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ 202 => Operation.response("Media", "application/json", Attachment), + 400 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError), 500 => Operation.response("Media", "application/json", ApiError) } diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index ec88eabe1..e4ce42f1c 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -195,7 +195,8 @@ defp notification_type do "pleroma:chat_mention", "pleroma:report", "move", - "follow_request" + "follow_request", + "poll" ], description: """ The type of event that resulted in the notification. diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index cae18c758..24d792916 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -115,7 +115,8 @@ def hashtag_operation do ], operationId: "TimelineController.hashtag", responses: %{ - 200 => Operation.response("Array of Status", "application/json", array_of_statuses()) + 200 => Operation.response("Array of Status", "application/json", array_of_statuses()), + 401 => Operation.response("Error", "application/json", ApiError) } } end diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex new file mode 100644 index 000000000..2a701066d --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex @@ -0,0 +1,285 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def emoji_operation do + %Operation{ + tags: ["Emojis"], + summary: "List all custom emojis", + operationId: "UtilController.emoji", + parameters: [], + responses: %{ + 200 => + Operation.response("List", "application/json", %Schema{ + type: :object, + additionalProperties: %Schema{ + type: :object, + properties: %{ + image_url: %Schema{type: :string}, + tags: %Schema{type: :array, items: %Schema{type: :string}} + } + }, + example: %{ + "firefox" => %{ + "image_url" => "/emoji/firefox.png", + "tag" => ["Fun"] + } + } + }) + } + } + end + + def frontend_configurations_operation do + %Operation{ + tags: ["Configuration"], + summary: "Dump frontend configurations", + operationId: "UtilController.frontend_configurations", + parameters: [], + responses: %{ + 200 => + Operation.response("List", "application/json", %Schema{ + type: :object, + additionalProperties: %Schema{type: :object} + }) + } + } + end + + def change_password_operation do + %Operation{ + tags: ["Account credentials"], + summary: "Change account password", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.change_password", + requestBody: request_body("Parameters", change_password_request(), required: true), + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp change_password_request do + %Schema{ + title: "ChangePasswordRequest", + description: "POST body for changing the account's passowrd", + type: :object, + required: [:password, :new_password, :new_password_confirmation], + properties: %{ + password: %Schema{type: :string, description: "Current password"}, + new_password: %Schema{type: :string, description: "New password"}, + new_password_confirmation: %Schema{ + type: :string, + description: "New password, confirmation" + } + } + } + end + + def change_email_operation do + %Operation{ + tags: ["Account credentials"], + summary: "Change account email", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.change_email", + requestBody: request_body("Parameters", change_email_request(), required: true), + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp change_email_request do + %Schema{ + title: "ChangeEmailRequest", + description: "POST body for changing the account's email", + type: :object, + required: [:email, :password], + properties: %{ + email: %Schema{ + type: :string, + description: "New email. Set to blank to remove the user's email." + }, + password: %Schema{type: :string, description: "Current password"} + } + } + end + + def update_notificaton_settings_operation do + %Operation{ + tags: ["Accounts"], + summary: "Update Notification Settings", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.update_notificaton_settings", + parameters: [ + Operation.parameter( + :block_from_strangers, + :query, + BooleanLike, + "blocks notifications from accounts you do not follow" + ), + Operation.parameter( + :hide_notification_contents, + :query, + BooleanLike, + "removes the contents of a message from the push notification" + ) + ], + requestBody: nil, + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 400 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def disable_account_operation do + %Operation{ + tags: ["Account credentials"], + summary: "Disable Account", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.disable_account", + parameters: [ + Operation.parameter(:password, :query, :string, "Password") + ], + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_account_operation do + %Operation{ + tags: ["Account credentials"], + summary: "Delete Account", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.delete_account", + parameters: [ + Operation.parameter(:password, :query, :string, "Password") + ], + requestBody: request_body("Parameters", delete_account_request(), required: false), + responses: %{ + 200 => + Operation.response("Success", "application/json", %Schema{ + type: :object, + properties: %{status: %Schema{type: :string, example: "success"}} + }), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def captcha_operation do + %Operation{ + summary: "Get a captcha", + operationId: "UtilController.captcha", + parameters: [], + responses: %{ + 200 => Operation.response("Success", "application/json", %Schema{type: :object}) + } + } + end + + def healthcheck_operation do + %Operation{ + tags: ["Accounts"], + summary: "Quick status check on the instance", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.healthcheck", + parameters: [], + responses: %{ + 200 => Operation.response("Healthy", "application/json", %Schema{type: :object}), + 503 => + Operation.response("Disabled or Unhealthy", "application/json", %Schema{type: :object}) + } + } + end + + def remote_subscribe_operation do + %Operation{ + tags: ["Accounts"], + summary: "Remote Subscribe", + operationId: "UtilController.remote_subscribe", + parameters: [], + responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})} + } + end + + def remote_interaction_operation do + %Operation{ + tags: ["Accounts"], + summary: "Remote interaction", + operationId: "UtilController.remote_interaction", + requestBody: request_body("Parameters", remote_interaction_request(), required: true), + responses: %{ + 200 => + Operation.response("Remote interaction URL", "application/json", %Schema{type: :object}) + } + } + end + + defp remote_interaction_request do + %Schema{ + title: "RemoteInteractionRequest", + description: "POST body for remote interaction", + type: :object, + required: [:ap_id, :profile], + properties: %{ + ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"}, + profile: %Schema{type: :string, description: "Remote profile webfinger"} + } + } + end + + defp delete_account_request do + %Schema{ + title: "AccountDeleteRequest", + description: "POST body for deleting one's own account", + type: :object, + properties: %{ + password: %Schema{ + type: :string, + description: "The user's own password for confirmation.", + format: :password + } + }, + example: %{ + "password" => "prettyp0ony1313" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/user_import_operation.ex b/lib/pleroma/web/api_spec/operations/user_import_operation.ex index 6292e2004..8df19f1fc 100644 --- a/lib/pleroma/web/api_spec/operations/user_import_operation.ex +++ b/lib/pleroma/web/api_spec/operations/user_import_operation.ex @@ -23,6 +23,7 @@ def follow_operation do requestBody: request_body("Parameters", import_request(), required: true), responses: %{ 200 => ok_response(), + 403 => Operation.response("Error", "application/json", ApiError), 500 => Operation.response("Error", "application/json", ApiError) }, security: [%{"oAuth" => ["write:follow"]}] diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index bd7143ab9..548e70544 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -194,9 +194,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do "id" => "9tKi3esbG7OQgZ2920", "muting" => false, "muting_notifications" => false, + "note" => "", "requested" => false, "showing_reblogs" => true, - "subscribing" => false + "subscribing" => false, + "notifying" => false }, "settings_store" => %{ "pleroma-fe" => %{} diff --git a/lib/pleroma/web/api_spec/schemas/account_relationship.ex b/lib/pleroma/web/api_spec/schemas/account_relationship.ex index 16b73ebb4..5d9e3b56e 100644 --- a/lib/pleroma/web/api_spec/schemas/account_relationship.ex +++ b/lib/pleroma/web/api_spec/schemas/account_relationship.ex @@ -22,9 +22,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do id: FlakeID, muting: %Schema{type: :boolean}, muting_notifications: %Schema{type: :boolean}, + note: %Schema{type: :string}, requested: %Schema{type: :boolean}, showing_reblogs: %Schema{type: :boolean}, - subscribing: %Schema{type: :boolean} + subscribing: %Schema{type: :boolean}, + notifying: %Schema{type: :boolean} }, example: %{ "blocked_by" => false, @@ -36,9 +38,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do "id" => "9tKi3esbG7OQgZ2920", "muting" => false, "muting_notifications" => false, + "note" => "", "requested" => false, "showing_reblogs" => true, - "subscribing" => false + "subscribing" => false, + "notifying" => false } }) end diff --git a/lib/pleroma/web/api_spec/schemas/boolean_like.ex b/lib/pleroma/web/api_spec/schemas/boolean_like.ex index 778158f66..94c5020ca 100644 --- a/lib/pleroma/web/api_spec/schemas/boolean_like.ex +++ b/lib/pleroma/web/api_spec/schemas/boolean_like.ex @@ -34,7 +34,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BooleanLike do def cast(%Cast{value: value} = context) do context - |> Map.put(:value, Pleroma.Web.ControllerHelper.truthy_param?(value)) + |> Map.put(:value, Pleroma.Web.Utils.Params.truthy_param?(value)) |> Cast.ok() end end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 3d042dc19..3caab0f00 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -282,9 +282,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do "id" => "9toJCsKN7SmSf3aj5c", "muting" => false, "muting_notifications" => false, + "note" => "", "requested" => false, "showing_reblogs" => true, - "subscribing" => false + "subscribing" => false, + "notifying" => false }, "skip_thread_containment" => false, "tags" => [] diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index 84741ee11..3fe9718c4 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -3,68 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.Authenticator do - alias Pleroma.Registration - alias Pleroma.User - - def implementation do - Pleroma.Config.get( - Pleroma.Web.Auth.Authenticator, - Pleroma.Web.Auth.PleromaAuthenticator - ) - end - - @callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()} - def get_user(plug), do: implementation().get_user(plug) - - @callback create_from_registration(Plug.Conn.t(), Registration.t()) :: + @callback get_user(Plug.Conn.t()) :: {:ok, user :: struct()} | {:error, any()} + @callback create_from_registration(Plug.Conn.t(), registration :: struct()) :: {:ok, User.t()} | {:error, any()} - def create_from_registration(plug, registration), - do: implementation().create_from_registration(plug, registration) - - @callback get_registration(Plug.Conn.t()) :: {:ok, Registration.t()} | {:error, any()} - def get_registration(plug), do: implementation().get_registration(plug) - + @callback get_registration(Plug.Conn.t()) :: {:ok, registration :: struct()} | {:error, any()} @callback handle_error(Plug.Conn.t(), any()) :: any() - def handle_error(plug, error), - do: implementation().handle_error(plug, error) - @callback auth_template() :: String.t() | nil - def auth_template do - # Note: `config :pleroma, :auth_template, "..."` support is deprecated - implementation().auth_template() || - Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) || - "show.html" - end - @callback oauth_consumer_template() :: String.t() | nil - def oauth_consumer_template do - implementation().oauth_consumer_template() || - Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html") - end - - @doc "Gets user by nickname or email for auth." - @spec fetch_user(String.t()) :: User.t() | nil - def fetch_user(name) do - User.get_by_nickname_or_email(name) - end - - # Gets name and password from conn - # - @spec fetch_credentials(Plug.Conn.t() | map()) :: - {:ok, {name :: any, password :: any}} | {:error, :invalid_credentials} - def fetch_credentials(%Plug.Conn{params: params} = _), - do: fetch_credentials(params) - - def fetch_credentials(params) do - case params do - %{"authorization" => %{"name" => name, "password" => password}} -> - {:ok, {name, password}} - - %{"grant_type" => "password", "username" => name, "password" => password} -> - {:ok, {name, password}} - - _ -> - {:error, :invalid_credentials} - end - end end diff --git a/lib/pleroma/web/auth/helpers.ex b/lib/pleroma/web/auth/helpers.ex new file mode 100644 index 000000000..c566de8d4 --- /dev/null +++ b/lib/pleroma/web/auth/helpers.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.Helpers do + alias Pleroma.User + + @doc "Gets user by nickname or email for auth." + @spec fetch_user(String.t()) :: User.t() | nil + def fetch_user(name) do + User.get_by_nickname_or_email(name) + end + + # Gets name and password from conn + # + @spec fetch_credentials(Plug.Conn.t() | map()) :: + {:ok, {name :: any, password :: any}} | {:error, :invalid_credentials} + def fetch_credentials(%Plug.Conn{params: params} = _), + do: fetch_credentials(params) + + def fetch_credentials(params) do + case params do + %{"authorization" => %{"name" => name, "password" => password}} -> + {:ok, {name, password}} + + %{"grant_type" => "password", "username" => name, "password" => password} -> + {:ok, {name, password}} + + _ -> + {:error, :invalid_credentials} + end + end +end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 17e08a2a6..f77e8d203 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -7,8 +7,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do require Logger - import Pleroma.Web.Auth.Authenticator, - only: [fetch_credentials: 1, fetch_user: 1] + import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] @behaviour Pleroma.Web.Auth.Authenticator @base Pleroma.Web.Auth.PleromaAuthenticator diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index 401f23c9f..68472e75f 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -8,8 +8,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do alias Pleroma.User alias Pleroma.Web.Plugs.AuthenticationPlug - import Pleroma.Web.Auth.Authenticator, - only: [fetch_credentials: 1, fetch_user: 1] + import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1] @behaviour Pleroma.Web.Auth.Authenticator diff --git a/lib/pleroma/web/auth/wrapper_authenticator.ex b/lib/pleroma/web/auth/wrapper_authenticator.ex new file mode 100644 index 000000000..c67082f7b --- /dev/null +++ b/lib/pleroma/web/auth/wrapper_authenticator.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.WrapperAuthenticator do + @behaviour Pleroma.Web.Auth.Authenticator + + defp implementation do + Pleroma.Config.get( + Pleroma.Web.Auth.Authenticator, + Pleroma.Web.Auth.PleromaAuthenticator + ) + end + + @impl true + def get_user(plug), do: implementation().get_user(plug) + + @impl true + def create_from_registration(plug, registration), + do: implementation().create_from_registration(plug, registration) + + @impl true + def get_registration(plug), do: implementation().get_registration(plug) + + @impl true + def handle_error(plug, error), + do: implementation().handle_error(plug, error) + + @impl true + def auth_template do + # Note: `config :pleroma, :auth_template, "..."` support is deprecated + implementation().auth_template() || + Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) || + "show.html" + end + + @impl true + def oauth_consumer_template do + implementation().oauth_consumer_template() || + Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html") + end +end diff --git a/lib/pleroma/web/channels/user_socket.ex b/lib/pleroma/web/channels/user_socket.ex index 1c09b6768..043206835 100644 --- a/lib/pleroma/web/channels/user_socket.ex +++ b/lib/pleroma/web/channels/user_socket.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Web.UserSocket do ## Channels # channel "room:*", Pleroma.Web.RoomChannel - channel("chat:*", Pleroma.Web.ChatChannel) + channel("chat:*", Pleroma.Web.ShoutChannel) # Socket params are passed from the client and can # be used to verify and authenticate a user. After @@ -22,7 +22,7 @@ defmodule Pleroma.Web.UserSocket do # See `Phoenix.Token` documentation for examples in # performing token verification on connect. def connect(%{"token" => token}, socket) do - with true <- Pleroma.Config.get([:chat, :enabled]), + with true <- Pleroma.Config.get([:shout, :enabled]), {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600), %User{} = user <- Pleroma.User.get_cached_by_id(user_id) do {:ok, assign(socket, :user_name, user.nickname)} diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 1b5f8491e..6f685cb7b 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -487,9 +487,7 @@ def remove_mute(user_id, activity_id) do else {what, result} = error -> Logger.warn( - "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{ - activity_id - }" + "CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}" ) {:error, error} diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 80a9fa7bb..b4e3e37ae 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Activity alias Pleroma.Conversation.Participation alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils @@ -213,8 +214,10 @@ defp object(draft) do emoji = Map.merge(emoji, summary_emoji) + {:ok, note_data, _meta} = Builder.note(draft) + object = - Utils.make_note_data(draft) + note_data |> Map.put("emoji", emoji) |> Map.put("source", draft.status) |> Map.put("generator", draft.params[:generator]) @@ -223,7 +226,7 @@ defp object(draft) do end defp preview?(draft) do - preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params[:preview]) + preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) %__MODULE__{draft | preview?: preview?} end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 94a378e11..b6feaf32a 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do import Pleroma.Web.Gettext - import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1] alias Calendar.Strftime alias Pleroma.Activity @@ -19,6 +18,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Web.CommonAPI.ActivityDraft alias Pleroma.Web.MediaProxy alias Pleroma.Web.Plugs.AuthenticationPlug + alias Pleroma.Web.Utils.Params require Logger require Pleroma.Constants @@ -69,7 +69,7 @@ def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public to = case visibility do "public" -> [Pleroma.Constants.as_public() | draft.mentions] - "local" -> [Pleroma.Constants.as_local_public() | draft.mentions] + "local" -> [Utils.as_local_public() | draft.mentions] end cc = [draft.user.follower_address] @@ -160,7 +160,7 @@ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data) |> DateTime.add(expires_in) |> DateTime.to_iso8601() - key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf" + key = if Params.truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf" poll = %{"type" => "Question", key => option_notes, "closed" => end_time} {:ok, {poll, emoji}} @@ -203,7 +203,7 @@ def make_content_html(%ActivityDraft{} = draft) do attachment_links = draft.params |> Map.get("attachment_links", Config.get([:instance, :attachment_links])) - |> truthy_param?() + |> Params.truthy_param?() content_type = get_content_type(draft.params[:content_type]) @@ -291,33 +291,6 @@ def format_input(text, "text/markdown", options) do |> Formatter.html_escape("text/html") end - def make_note_data(%ActivityDraft{} = draft) do - %{ - "type" => "Note", - "to" => draft.to, - "cc" => draft.cc, - "content" => draft.content_html, - "summary" => draft.summary, - "sensitive" => draft.sensitive, - "context" => draft.context, - "attachment" => draft.attachments, - "actor" => draft.user.ap_id, - "tag" => Keyword.values(draft.tags) |> Enum.uniq() - } - |> add_in_reply_to(draft.in_reply_to) - |> Map.merge(draft.extra) - end - - defp add_in_reply_to(object, nil), do: object - - defp add_in_reply_to(object, in_reply_to) do - with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do - Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) - else - _ -> object - end - end - def format_naive_asctime(date) do date |> DateTime.from_naive!("Etc/UTC") |> format_asctime end @@ -412,19 +385,14 @@ def maybe_notify_mentioned_recipients( def maybe_notify_mentioned_recipients(recipients, _), do: recipients - # Do not notify subscribers if author is making a reply - def maybe_notify_subscribers(recipients, %Activity{ - object: %Object{data: %{"inReplyTo" => _ap_id}} - }) do - recipients - end - def maybe_notify_subscribers( recipients, - %Activity{data: %{"actor" => actor, "type" => type}} = activity - ) - when type == "Create" do - with %User{} = user <- User.get_cached_by_ap_id(actor) do + %Activity{data: %{"actor" => actor, "type" => "Create"}} = activity + ) do + # Do not notify subscribers if author is making a reply + with %Object{data: object} <- Object.normalize(activity, fetch: false), + nil <- object["inReplyTo"], + %User{} = user <- User.get_cached_by_ap_id(actor) do subscriber_ids = user |> User.subscriber_users() diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 61d65e7a3..7b84b43e4 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -6,17 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller alias Pleroma.Pagination - - # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html - @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] - - def explicitly_falsy_param?(value), do: value in @falsy_param_values - - # Note: `nil` and `""` are considered falsy values in Pleroma - def falsy_param?(value), - do: explicitly_falsy_param?(value) or value in [nil, ""] - - def truthy_param?(value), do: not falsy_param?(value) + alias Pleroma.Web.Utils.Params def json_response(conn, status, _) when status in [204, :no_content] do conn @@ -123,6 +113,6 @@ def embed_relationships?(params) do # To do once OpenAPI transition mess is over: just `truthy_param?(params[:with_relationships])` params |> Map.get(:with_relationships, params["with_relationships"]) - |> truthy_param?() + |> Params.truthy_param?() end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 8e274de88..75484fac5 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.Endpoint do alias Pleroma.Config socket("/socket", Pleroma.Web.UserSocket) + socket("/live", Phoenix.LiveView.Socket) plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index f5ef76d32..69cfc2d52 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -96,6 +96,11 @@ def perform(:incoming_ap_doc, params) do Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") {:error, e} + {:error, {:validate_object, _}} = e -> + Logger.error("Incoming AP doc validation error: #{inspect(e)}") + Logger.debug(Jason.encode!(params, pretty: true)) + e + e -> # Just drop those for now Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex index 66940f311..c0fb35e01 100644 --- a/lib/pleroma/web/feed/feed_view.ex +++ b/lib/pleroma/web/feed/feed_view.ex @@ -52,10 +52,10 @@ def most_recent_update(activities, user) do def feed_logo do case Pleroma.Config.get([:feed, :logo]) do nil -> - "#{Pleroma.Web.base_url()}/static/logo.svg" + "#{Pleroma.Web.Endpoint.url()}/static/logo.svg" logo -> - "#{Pleroma.Web.base_url()}#{logo}" + "#{Pleroma.Web.Endpoint.url()}#{logo}" end |> MediaProxy.url() end diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 58d35da1e..739b1f026 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -18,6 +18,8 @@ defmodule Pleroma.Web.Feed.UserController do def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do Pleroma.Web.Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) + else + _ -> Pleroma.Web.Fallback.RedirectController.redirector(conn, nil) end end @@ -28,7 +30,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params) def feed_redirect(conn, %{"nickname" => nickname}) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do - redirect(conn, external: "#{user_feed_url(conn, :feed, user.nickname)}.atom") + redirect(conn, external: "#{Routes.user_feed_url(conn, :feed, user.nickname)}.atom") end end diff --git a/lib/pleroma/web/manifest_controller.ex b/lib/pleroma/web/manifest_controller.ex new file mode 100644 index 000000000..52589540b --- /dev/null +++ b/lib/pleroma/web/manifest_controller.ex @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ManifestController do + use Pleroma.Web, :controller + + plug(:skip_auth when action == :show) + + @doc "GET /manifest.json" + def show(conn, _params) do + render(conn, "manifest.json") + end +end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex deleted file mode 100644 index e788ab37a..000000000 --- a/lib/pleroma/web/masto_fe_controller.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MastoFEController do - use Pleroma.Web, :controller - - alias Pleroma.User - alias Pleroma.Web.MastodonAPI.AuthController - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug - - plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings) - - # Note: :index action handles attempt of unauthenticated access to private instance with redirect - plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action == :index) - - plug( - OAuthScopesPlug, - %{scopes: ["read"], fallback: :proceed_unauthenticated} - when action == :index - ) - - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :manifest - ) - - @doc "GET /web/*path" - def index(conn, _params) do - with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn, - {:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do - conn - |> put_layout(false) - |> render("index.html", - token: token.token, - user: user, - custom_emojis: Pleroma.Emoji.get_all() - ) - else - _ -> - conn - |> put_session(:return_to, conn.request_path) - |> redirect(to: "/web/login") - end - end - - @doc "GET /web/manifest.json" - def manifest(conn, _params) do - render(conn, "manifest.json") - end - - @doc "PUT /api/web/settings: Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere" - def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do - with {:ok, _} <- User.mastodon_settings_update(user, settings) do - json(conn, %{}) - else - e -> - conn - |> put_status(:internal_server_error) - |> json(%{error: inspect(e)}) - 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 7a1e99044..5dfbecf5a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [ add_link_headers: 2, - truthy_param?: 1, assign_account_by_id: 2, embed_relationships?: 1, json_response: 3 @@ -16,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Maps alias Pleroma.User + alias Pleroma.UserNote alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline @@ -25,16 +25,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.OAuth.OAuthController - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.Utils.Params plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create) + plug(:skip_auth when action == :create) - plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses]) + plug(:skip_public_check when action in [:show, :statuses]) plug( OAuthScopesPlug, @@ -54,7 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do when action in [:verify_credentials, :endorsements, :identity_proofs] ) - plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials) + plug( + OAuthScopesPlug, + %{scopes: ["write:accounts"]} + when action in [:update_credentials, :note] + ) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists) @@ -80,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) @relationship_actions [:follow, :unfollow] - @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a + @needs_account ~W(followers following lists follow unfollow mute unmute block unblock note)a plug( RateLimiter, @@ -188,7 +192,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p :accepts_chat_messages ] |> Enum.reduce(%{}, fn key, acc -> - Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) + Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)}) end) |> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:bio, params[:note]) @@ -436,6 +440,16 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do end end + @doc "POST /api/v1/accounts/:id/note" + def note( + %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn, + _params + ) do + with {:ok, _user_note} <- UserNote.create(noter, target, comment) do + render(conn, "relationship.json", user: noter, target: target) + end + end + @doc "POST /api/v1/follows" def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do case User.get_cached_by_nickname(uri) do diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex index dd3b39c77..93e63ba03 100644 --- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex @@ -14,21 +14,13 @@ defmodule Pleroma.Web.MastodonAPI.AppController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - alias Pleroma.Web.Plugs.OAuthScopesPlug action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] - when action in [:create, :verify_credentials] - ) + plug(:skip_auth when action in [:create, :verify_credentials]) plug(Pleroma.Web.ApiSpec.CastAndValidate) - @local_mastodon_name "Mastodon-Local" - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation @doc "POST /api/v1/apps" @@ -41,7 +33,6 @@ def create(%{body_params: params} = conn, _params) do |> Map.put(:scopes, scopes) with cs <- App.register_changeset(%App{}, app_attrs), - false <- cs.changes[:client_name] == @local_mastodon_name, {:ok, app} <- Repo.insert(cs) do render(conn, "show.json", app: app) end diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex index eb6639fc5..08943f6f1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -7,77 +7,12 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] - alias Pleroma.Helpers.AuthHelper - alias Pleroma.Helpers.UriHelper - alias Pleroma.User - alias Pleroma.Web.OAuth.App - alias Pleroma.Web.OAuth.Authorization - alias Pleroma.Web.OAuth.Token - alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken alias Pleroma.Web.TwitterAPI.TwitterAPI action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset) - @local_mastodon_name "Mastodon-Local" - - @doc "GET /web/login" - # Local Mastodon FE login callback action - def login(conn, %{"code" => auth_token} = params) do - with {:ok, app} <- local_mastofe_app(), - {:ok, auth} <- Authorization.get_by_token(app, auth_token), - {:ok, oauth_token} <- Token.exchange_token(app, auth) do - redirect_to = - conn - |> local_mastodon_post_login_path() - |> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token}) - - conn - |> AuthHelper.put_session_token(oauth_token.token) - |> redirect(to: redirect_to) - else - _ -> redirect_to_oauth_form(conn, params) - end - end - - def login(conn, params) do - with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn, - {:ok, %{id: ^app_id}} <- local_mastofe_app() do - redirect(conn, to: local_mastodon_post_login_path(conn)) - else - _ -> redirect_to_oauth_form(conn, params) - end - end - - defp redirect_to_oauth_form(conn, _params) do - with {:ok, app} <- local_mastofe_app() do - path = - o_auth_path(conn, :authorize, - response_type: "code", - client_id: app.client_id, - redirect_uri: ".", - scope: Enum.join(app.scopes, " ") - ) - - redirect(conn, to: path) - end - end - - @doc "DELETE /auth/sign_out" - def logout(conn, _) do - conn = - with %{assigns: %{token: %Token{} = oauth_token}} <- conn, - session_token = AuthHelper.get_session_token(conn), - {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do - AuthHelper.delete_session_token(conn) - else - _ -> conn - end - - redirect(conn, to: "/") - end - @doc "POST /auth/password" def password_reset(conn, params) do nickname_or_email = params["email"] || params["nickname"] @@ -86,23 +21,4 @@ def password_reset(conn, params) do json_response(conn, :no_content, "") end - - defp local_mastodon_post_login_path(conn) do - case get_session(conn, :return_to) do - nil -> - masto_fe_path(conn, :index, ["getting-started"]) - - return_to -> - delete_session(conn, :return_to) - return_to - end - end - - @spec local_mastofe_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} - def local_mastofe_app do - App.get_or_make( - %{client_name: @local_mastodon_name, redirect_uris: "."}, - ["read", "write", "follow", "push", "admin"] - ) - end end diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex index d7e18dc92..31b647755 100644 --- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex @@ -7,11 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug( - :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] - when action == :index - ) + plug(:skip_auth when action == :index) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 63d0e2c35..d915298f1 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -9,7 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.Plugs.OAuthScopesPlug - plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:assign_follower when action != :index) diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index c7a5267d4..5376e4594 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -7,11 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug( - :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] - when action in [:show, :peers] - ) + plug(:skip_auth when action in [:show, :peers]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index a1bcc91d9..a0f79f377 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -15,11 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - plug( - :skip_plug, - [Pleroma.Web.Plugs.OAuthScopesPlug, Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug] - when action in [:empty_array, :empty_object] - ) + plug(:skip_auth when action in [:empty_array, :empty_object]) action_fallback(Pleroma.Web.MastodonAPI.FallbackController) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index d6949ed80..5918b288d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2]) plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show) diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 647ba661e..002d6b2ce 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -50,6 +50,7 @@ def index(conn, %{account_id: account_id} = params) do favourite move pleroma:emoji_reaction + poll } def index(%{assigns: %{user: user}} = conn, params) do params = diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index af93e453d..1459fc492 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do alias Pleroma.Activity alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.Endpoint alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.Plugs.OAuthScopesPlug @@ -17,6 +17,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do require Logger + @search_limit 40 + plug(Pleroma.Web.ApiSpec.CastAndValidate) # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) @@ -77,7 +79,7 @@ defp search_options(params, user) do [ resolve: params[:resolve], following: params[:following], - limit: params[:limit], + limit: min(params[:limit], @search_limit), offset: params[:offset], type: params[:type], author: get_author(params), @@ -108,7 +110,7 @@ defp resource_search(_, "statuses", query, options) do end defp resource_search(:v2, "hashtags", query, options) do - tags_path = Web.base_url() <> "/tag/" + tags_path = Endpoint.url() <> "/tag/" query |> prepare_tags(options) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 724dc5c5d..2eff4d9d0 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -27,10 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug( - :skip_plug, - Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show] - ) + plug(:skip_public_check when action in [:index, :show]) @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} diff --git a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex index 01e122dd9..e913fcf4b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/suggestion_controller.ex @@ -4,11 +4,16 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionController do use Pleroma.Web, :controller + import Ecto.Query + alias Pleroma.FollowingRelationship + alias Pleroma.User + alias Pleroma.UserRelationship require Logger plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action == :index) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:index, :index2]) + plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["write"]} when action in [:dismiss]) def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -26,7 +31,90 @@ def index_operation do } end + def index2_operation do + %OpenApiSpex.Operation{ + tags: ["Suggestions"], + summary: "Follow suggestions", + operationId: "SuggestionController.index2", + responses: %{ + 200 => Pleroma.Web.ApiSpec.Helpers.empty_array_response() + } + } + end + + def dismiss_operation do + %OpenApiSpex.Operation{ + tags: ["Suggestions"], + summary: "Remove a suggestion", + operationId: "SuggestionController.dismiss", + parameters: [ + OpenApiSpex.Operation.parameter( + :account_id, + :path, + %OpenApiSpex.Schema{type: :string}, + "Account to dismiss", + required: true + ) + ], + responses: %{ + 200 => Pleroma.Web.ApiSpec.Helpers.empty_object_response() + } + } + end + @doc "GET /api/v1/suggestions" def index(conn, params), do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params) + + @doc "GET /api/v2/suggestions" + def index2(%{assigns: %{user: user}} = conn, params) do + limit = Map.get(params, :limit, 40) |> min(80) + + users = + %{is_suggested: true, invisible: false, limit: limit} + |> User.Query.build() + |> exclude_user(user) + |> exclude_relationships(user, [:block, :mute, :suggestion_dismiss]) + |> exclude_following(user) + |> Pleroma.Repo.all() + + render(conn, "index.json", %{ + users: users, + source: :staff, + for: user, + skip_visibility_check: true + }) + end + + defp exclude_user(query, %User{id: user_id}) do + where(query, [u], u.id != ^user_id) + end + + defp exclude_relationships(query, %User{id: user_id}, relationship_types) do + query + |> join(:left, [u], r in UserRelationship, + as: :user_relationships, + on: + r.target_id == u.id and r.source_id == ^user_id and + r.relationship_type in ^relationship_types + ) + |> where([user_relationships: r], is_nil(r.target_id)) + end + + defp exclude_following(query, %User{id: user_id}) do + query + |> join(:left, [u], r in FollowingRelationship, + as: :following_relationships, + on: r.following_id == u.id and r.follower_id == ^user_id and r.state == :follow_accept + ) + |> where([following_relationships: r], is_nil(r.following_id)) + end + + @doc "DELETE /api/v1/suggestions/:account_id" + def dismiss(%{assigns: %{user: source}} = conn, %{account_id: user_id}) do + with %User{} = target <- User.get_cached_by_id(user_id), + {:ok, _} <- UserRelationship.create(:suggestion_dismiss, source, target) do + json(conn, %{}) + end + 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 c611958be..10c279893 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -12,12 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Pagination alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:public, :hashtag]) + plug(:skip_public_check when action in [:public, :hashtag]) # TODO: Replace with a macro when there is a Phoenix release with the following commit in it: # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e @@ -37,8 +36,6 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do when action in [:public, :hashtag] ) - plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation # GET /api/v1/timelines/home @@ -196,7 +193,9 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() - render(conn, "index.json", + conn + |> add_link_headers(activities) + |> render("index.json", activities: activities, for: user, as: :activity, diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 71479550e..23846b36a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -24,6 +24,7 @@ def follow(follower, followed, params \\ %{}) do with {:ok, follower, _followed, _} <- result do options = cast_params(params) set_reblogs_visibility(options[:reblogs], result) + set_subscription(options[:notify], result) {:ok, follower} end end @@ -36,6 +37,16 @@ defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do CommonAPI.show_reblogs(follower, followed) end + defp set_subscription(true, {:ok, follower, followed, _}) do + User.subscribe(follower, followed) + end + + defp set_subscription(false, {:ok, follower, followed, _}) do + User.unsubscribe(follower, followed) + end + + defp set_subscription(_, _), do: {:ok, nil} + @spec get_followers(User.t(), map()) :: list(User.t()) def get_followers(user, params \\ %{}) do user @@ -73,7 +84,8 @@ defp cast_params(params) do exclude_visibilities: {:array, :string}, reblogs: :boolean, with_muted: :boolean, - account_ap_id: :string + account_ap_id: :string, + notify: :boolean } changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index ac25aefdd..3c8dd0353 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do alias Pleroma.FollowingRelationship alias Pleroma.User + alias Pleroma.UserNote alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView @@ -101,6 +102,15 @@ def render( User.following?(target, reading_user) end + subscribing = + UserRelationship.exists?( + user_relationships, + :inverse_subscription, + target, + reading_user, + &User.subscribed_to?(&2, &1) + ) + # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags %{ id: to_string(target.id), @@ -138,14 +148,8 @@ def render( target, &User.muted_notifications?(&1, &2) ), - subscribing: - UserRelationship.exists?( - user_relationships, - :inverse_subscription, - target, - reading_user, - &User.subscribed_to?(&2, &1) - ), + subscribing: subscribing, + notifying: subscribing, requested: follow_state == :follow_pending, domain_blocking: User.blocks_domain?(reading_user, target), showing_reblogs: @@ -156,7 +160,12 @@ def render( target, &User.muting_reblogs?(&1, &2) ), - endorsed: false + endorsed: false, + note: + UserNote.show( + reading_user, + target + ) } end @@ -269,6 +278,7 @@ defp do_render("show.json", %{user: user} = opts) do ap_id: user.ap_id, also_known_as: user.also_known_as, is_confirmed: user.is_confirmed, + is_suggested: user.is_suggested, tags: user.tags, hide_followers_count: user.hide_followers_count, hide_follows_count: user.hide_follows_count, @@ -292,6 +302,7 @@ defp do_render("show.json", %{user: user} = opts) do |> maybe_put_allow_following_move(user, opts[:for]) |> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_notification_count(user, opts[:for]) + |> maybe_put_email_address(user, opts[:for]) end defp username_from_nickname(string) when is_binary(string) do @@ -403,6 +414,16 @@ defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: use defp maybe_put_unread_notification_count(data, _, _), do: data + defp maybe_put_email_address(data, %User{id: user_id}, %User{id: user_id} = user) do + Kernel.put_in( + data, + [:pleroma, :email], + user.email + ) + end + + defp maybe_put_email_address(data, _, _), do: data + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(_), do: nil end diff --git a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex index 40e314164..7d2d605e9 100644 --- a/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex +++ b/lib/pleroma/web/mastodon_api/views/custom_emoji_view.ex @@ -6,14 +6,14 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiView do use Pleroma.Web, :view alias Pleroma.Emoji - alias Pleroma.Web + alias Pleroma.Web.Endpoint def render("index.json", %{custom_emojis: custom_emojis}) do render_many(custom_emojis, __MODULE__, "show.json") end def render("show.json", %{custom_emoji: {shortcode, %Emoji{file: relative_url, tags: tags}}}) do - url = Web.base_url() |> URI.merge(relative_url) |> to_string() + url = Endpoint.url() |> URI.merge(relative_url) |> to_string() %{ "shortcode" => shortcode, diff --git a/lib/pleroma/web/mastodon_api/views/follow_request_view.ex b/lib/pleroma/web/mastodon_api/views/follow_request_view.ex new file mode 100644 index 000000000..4c7d9fc65 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/follow_request_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.FollowRequestView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.AccountView.render(view, opts) +end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index dac68d8e6..ec7d150a9 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -14,7 +14,7 @@ def render("show.json", _) do instance = Config.get(:instance) %{ - uri: Pleroma.Web.base_url(), + uri: Pleroma.Web.Endpoint.url(), title: Keyword.get(instance, :name), description: Keyword.get(instance, :description), version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", @@ -24,7 +24,8 @@ def render("show.json", _) do }, stats: Pleroma.Stats.get_stats(), thumbnail: - URI.merge(Pleroma.Web.base_url(), Keyword.get(instance, :instance_thumbnail)) |> to_string, + URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail)) + |> to_string, languages: ["en"], registrations: Keyword.get(instance, :registrations_open), approval_required: Keyword.get(instance, :account_approval_required), @@ -35,8 +36,8 @@ def render("show.json", _) do avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), - background_image: Pleroma.Web.base_url() <> Keyword.get(instance, :background_image), - chat_limit: Keyword.get(instance, :chat_limit), + background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image), + shout_limit: Config.get([:shout, :limit]), description_limit: Keyword.get(instance, :description_limit), pleroma: %{ metadata: %{ @@ -58,6 +59,7 @@ def features do "mastodon_api", "mastodon_api_streaming", "polls", + "v2_suggestions", "pleroma_explicit_addressing", "shareable_emoji_packs", "multifetch", @@ -68,9 +70,13 @@ def features do if Config.get([:gopher, :enabled]) do "gopher" end, - if Config.get([:chat, :enabled]) do + # backwards compat + if Config.get([:shout, :enabled]) do "chat" end, + if Config.get([:shout, :enabled]) do + "shout" + end, if Config.get([:instance, :allow_relay]) do "relay" end, @@ -78,7 +84,10 @@ def features do "safe_dm_mentions" end, "pleroma_emoji_reactions", - "pleroma_chat_messages" + "pleroma_chat_messages", + if Config.get([:instance, :show_reactions]) do + "exposable_reactions" + end ] |> Enum.filter(& &1) end @@ -90,7 +99,20 @@ def federation do {:ok, data} = MRF.describe() data - |> Map.merge(%{quarantined_instances: quarantined}) + |> Map.put( + :quarantined_instances, + Enum.map(quarantined, fn {instance, _reason} -> instance end) + ) + # This is for backwards compatibility. We originally didn't sent + # extra info like a reason why an instance was rejected/quarantined/etc. + # Because we didn't want to break backwards compatibility it was decided + # to add an extra "info" key. + |> Map.put(:quarantined_instances_info, %{ + "quarantined_instances" => + quarantined + |> Enum.map(fn {instance, reason} -> {instance, %{"reason" => reason}} end) + |> Map.new() + }) else %{} end diff --git a/lib/pleroma/web/mastodon_api/views/media_view.ex b/lib/pleroma/web/mastodon_api/views/media_view.ex new file mode 100644 index 000000000..cf521887e --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/media_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MediaView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.StatusView.render(view, opts) +end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index df9bedfed..35c636d4e 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -112,6 +112,9 @@ def render( "move" -> put_target(response, activity, reading_user, %{}) + "poll" -> + put_status(response, activity, reading_user, status_render_opts) + "pleroma:emoji_reaction" -> response |> put_status(parent_activity_fn.(), reading_user, status_render_opts) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 814b3d142..463f34198 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do alias Pleroma.Activity alias Pleroma.HTML + alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -64,11 +65,19 @@ defp get_context_id(%{data: %{"context" => context}}) when is_binary(context), defp get_context_id(_), do: nil - defp reblogged?(activity, user) do - object = Object.normalize(activity, fetch: false) || %{} - present?(user && user.ap_id in (object.data["announcements"] || [])) + # Check if the user reblogged this status + defp reblogged?(activity, %User{ap_id: ap_id}) do + with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <- + Object.normalize(activity, fetch: false) do + ap_id in announcements + else + _ -> false + end end + # False if the user is logged out + defp reblogged?(_activity, _user), do: false + def render("index.json", opts) do reading_user = opts[:for] @@ -259,7 +268,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} content_html = content - |> HTML.get_cached_scrubbed_html_for_activity( + |> Activity.HTML.get_cached_scrubbed_html_for_activity( User.html_filter_policy(opts[:for]), activity, "mastoapi:content" @@ -267,7 +276,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} content_plaintext = content - |> HTML.get_cached_stripped_html_for_activity( + |> Activity.HTML.get_cached_stripped_html_for_activity( activity, "mastoapi:content" ) @@ -417,6 +426,7 @@ def render("attachment.json", %{attachment: attachment}) do media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" href = attachment_url["href"] |> MediaProxy.url() href_preview = attachment_url["href"] |> MediaProxy.preview_url() + meta = render("attachment_meta.json", %{attachment: attachment}) type = cond do @@ -439,8 +449,24 @@ def render("attachment.json", %{attachment: attachment}) do pleroma: %{mime_type: media_type}, blurhash: attachment["blurhash"] } + |> Maps.put_if_present(:meta, meta) end + def render("attachment_meta.json", %{ + attachment: %{"url" => [%{"width" => width, "height" => height} | _]} + }) + when is_integer(width) and is_integer(height) do + %{ + original: %{ + width: width, + height: height, + aspect: width / height + } + } + end + + def render("attachment_meta.json", _), do: nil + def render("context.json", %{activity: activity, activities: activities, user: user}) do %{ancestors: ancestors, descendants: descendants} = activities @@ -496,7 +522,7 @@ def render_content(object), do: object.data["content"] || "" def build_tags(object_tags) when is_list(object_tags) do object_tags |> Enum.filter(&is_binary/1) - |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.base_url()}/tag/#{URI.encode(&1)}"}) + |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"}) end def build_tags(_), do: [] diff --git a/lib/pleroma/web/mastodon_api/views/suggestion_view.ex b/lib/pleroma/web/mastodon_api/views/suggestion_view.ex new file mode 100644 index 000000000..865229a88 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/suggestion_view.ex @@ -0,0 +1,28 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SuggestionView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI.AccountView + + @source_types [:staff, :global, :past_interactions] + + def render("index.json", %{users: users} = opts) do + Enum.map(users, fn user -> + opts = + opts + |> Map.put(:user, user) + |> Map.delete(:users) + + render("show.json", opts) + end) + end + + def render("show.json", %{source: source, user: _user} = opts) when source in @source_types do + %{ + source: source, + account: AccountView.render("show.json", opts) + } + end +end diff --git a/lib/pleroma/web/mastodon_api/views/timeline_view.ex b/lib/pleroma/web/mastodon_api/views/timeline_view.ex new file mode 100644 index 000000000..91226d78e --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/timeline_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.TimelineView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.StatusView.render(view, opts) +end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 0d1faffbd..b978167b6 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -49,9 +49,7 @@ def init(%{qs: qs} = req, state) do def websocket_init(state) do Logger.debug( - "#{__MODULE__} accepted websocket connection for user #{ - (state.user || %{id: "anonymous"}).id - }, topic #{state.topic}" + "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}" ) Streamer.add_socket(state.topic, state.user) @@ -106,9 +104,7 @@ def terminate(_reason, _req, []), do: :ok def terminate(reason, _req, state) do Logger.debug( - "#{__MODULE__} terminating websocket connection for user #{ - (state.user || %{id: "anonymous"}).id - }, topic #{state.topic || "?"}: #{inspect(reason)}" + "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic || "?"}: #{inspect(reason)}" ) Streamer.remove_socket(state.topic) diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex index d0d4bb4b3..0b232f14b 100644 --- a/lib/pleroma/web/media_proxy.ex +++ b/lib/pleroma/web/media_proxy.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MediaProxy do alias Pleroma.Config alias Pleroma.Helpers.UriHelper alias Pleroma.Upload - alias Pleroma.Web + alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy.Invalidation @base64_opts [padding: false] @@ -69,7 +69,7 @@ def enabled?, do: Config.get([:media_proxy, :enabled], false) # non-local non-whitelisted URLs through it and be sure that body size constraint is preserved. def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :enabled]) - def local?(url), do: String.starts_with?(url, Web.base_url()) + def local?(url), do: String.starts_with?(url, Endpoint.url()) def whitelisted?(url) do %{host: domain} = URI.parse(url) @@ -127,7 +127,7 @@ def decode_url(encoded) do end defp signed_url(url) do - :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) + :crypto.mac(:hmac, :sha, Config.get([Endpoint, :secret_key_base]), url) end def filename(url_or_path) do @@ -135,7 +135,7 @@ def filename(url_or_path) do end def base_url do - Config.get([:media_proxy, :base_url], Web.base_url()) + Config.get([:media_proxy, :base_url], Endpoint.url()) end defp proxy_url(path, sig_base64, url_base64, filename) do diff --git a/lib/pleroma/web/metadata/providers/open_graph.ex b/lib/pleroma/web/metadata/providers/open_graph.ex index 1687b2634..df0cca74a 100644 --- a/lib/pleroma/web/metadata/providers/open_graph.ex +++ b/lib/pleroma/web/metadata/providers/open_graph.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do alias Pleroma.User + alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata.Providers.Provider alias Pleroma.Web.Metadata.Utils @@ -19,37 +20,24 @@ def build_tags(%{ }) do attachments = build_attachments(object) scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - ": “" <> scrubbed_content <> "”" - else - "" - end - # Most previews only show og:title which is inconvenient. Instagram - # hacks this by putting the description in the title and making the - # description longer prefixed by how many likes and shares the post - # has. Here we use the descriptive nickname in the title, and expand - # the full account & nickname in the description. We also use the cute^Wevil - # smart quotes around the status text like Instagram, too. [ {:meta, [ property: "og:title", - content: "#{user.name}" <> content + content: Utils.user_name_string(user) ], []}, {:meta, [property: "og:url", content: url], []}, {:meta, [ property: "og:description", - content: "#{Utils.user_name_string(user)}" <> content + content: scrubbed_content ], []}, - {:meta, [property: "og:type", content: "website"], []} + {:meta, [property: "og:type", content: "article"], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], + {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} @@ -70,8 +58,9 @@ def build_tags(%{user: user}) do ], []}, {:meta, [property: "og:url", content: user.uri || user.ap_id], []}, {:meta, [property: "og:description", content: truncated_bio], []}, - {:meta, [property: "og:type", content: "website"], []}, - {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + {:meta, [property: "og:type", content: "article"], []}, + {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))], + []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} ] @@ -82,29 +71,35 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. - # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image + # TODO: Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ - {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []} | acc ] + # Not using preview_url for this. It saves bandwidth, but the image dimensions will + # be wrong. We generate it on the fly and have no way to capture or analyze the + # image to get the dimensions. This can be an issue for apps/FEs rendering images + # in timelines too, but you can get clever with the aspect ratio metadata as a + # workaround. "image" -> [ - {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, - {:meta, [property: "og:image:width", content: 150], []}, - {:meta, [property: "og:image:height", content: 150], []} + {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []}, + {:meta, [property: "og:image:alt", content: attachment["name"]], []} | acc ] + |> maybe_add_dimensions(url) "video" -> [ - {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []} | acc ] + |> maybe_add_dimensions(url) + |> maybe_add_video_thumbnail(url) _ -> acc @@ -116,4 +111,38 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do end defp build_attachments(_), do: [] + + # We can use url["mediaType"] to dynamically fill the metadata + defp maybe_add_dimensions(metadata, url) do + type = url["mediaType"] |> String.split("/") |> List.first() + + cond do + !is_nil(url["height"]) && !is_nil(url["width"]) -> + metadata ++ + [ + {:meta, [property: "og:#{type}:width", content: "#{url["width"]}"], []}, + {:meta, [property: "og:#{type}:height", content: "#{url["height"]}"], []} + ] + + true -> + metadata + end + end + + # Media Preview Proxy makes thumbnails of videos without resizing, so we can trust the + # width and height of the source video. + defp maybe_add_video_thumbnail(metadata, url) do + cond do + Pleroma.Config.get([:media_preview_proxy, :enabled], false) -> + metadata ++ + [ + {:meta, [property: "og:image:width", content: "#{url["width"]}"], []}, + {:meta, [property: "og:image:height", content: "#{url["height"]}"], []}, + {:meta, [property: "og:image", content: MediaProxy.preview_url(url["href"])], []} + ] + + true -> + metadata + end + end end diff --git a/lib/pleroma/web/metadata/providers/twitter_card.ex b/lib/pleroma/web/metadata/providers/twitter_card.ex index 58fc05cf9..79183df86 100644 --- a/lib/pleroma/web/metadata/providers/twitter_card.ex +++ b/lib/pleroma/web/metadata/providers/twitter_card.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do alias Pleroma.User + alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata.Providers.Provider alias Pleroma.Web.Metadata.Utils @@ -16,17 +17,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do def build_tags(%{activity_id: id, object: object, user: user}) do attachments = build_attachments(id, object) scrubbed_content = Utils.scrub_html_and_truncate(object) - # Zero width space - content = - if scrubbed_content != "" and scrubbed_content != "\u200B" do - "“" <> scrubbed_content <> "”" - else - "" - end [ title_tag(user), - {:meta, [property: "twitter:description", content: content], []} + {:meta, [property: "twitter:description", content: scrubbed_content], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ @@ -55,14 +49,14 @@ defp title_tag(user) do end def image_tag(user) do - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + {:meta, [property: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], + []} end defp build_attachments(id, %{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - # TODO: Add additional properties to objects when we have the data available. case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ @@ -73,25 +67,37 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do | acc ] + # Not using preview_url for this. It saves bandwidth, but the image dimensions will + # be wrong. We generate it on the fly and have no way to capture or analyze the + # image to get the dimensions. This can be an issue for apps/FEs rendering images + # in timelines too, but you can get clever with the aspect ratio metadata as a + # workaround. "image" -> [ {:meta, [property: "twitter:card", content: "summary_large_image"], []}, {:meta, [ property: "twitter:player", - content: Utils.attachment_url(url["href"]) + content: MediaProxy.url(url["href"]) ], []} | acc ] + |> maybe_add_dimensions(url) - # TODO: Need the true width and height values here or Twitter renders an iFrame with - # a bad aspect ratio "video" -> + # fallback to old placeholder values + height = url["height"] || 480 + width = url["width"] || 480 + [ {:meta, [property: "twitter:card", content: "player"], []}, {:meta, [property: "twitter:player", content: player_url(id)], []}, - {:meta, [property: "twitter:player:width", content: "480"], []}, - {:meta, [property: "twitter:player:height", content: "480"], []} + {:meta, [property: "twitter:player:width", content: "#{width}"], []}, + {:meta, [property: "twitter:player:height", content: "#{height}"], []}, + {:meta, [property: "twitter:player:stream", content: MediaProxy.url(url["href"])], + []}, + {:meta, + [property: "twitter:player:stream:content_type", content: url["mediaType"]], []} | acc ] @@ -109,4 +115,20 @@ defp build_attachments(_id, _object), do: [] defp player_url(id) do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) end + + # Videos have problems without dimensions, but we used to not provide WxH for images. + # A default (read: incorrect) fallback for images is likely to cause rendering bugs. + defp maybe_add_dimensions(metadata, url) do + cond do + !is_nil(url["height"]) && !is_nil(url["width"]) -> + metadata ++ + [ + {:meta, [property: "twitter:player:width", content: "#{url["width"]}"], []}, + {:meta, [property: "twitter:player:height", content: "#{url["height"]}"], []} + ] + + true -> + metadata + end + end end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index de7195435..caca42934 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -3,17 +3,17 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Metadata.Utils do + alias Pleroma.Activity alias Pleroma.Emoji alias Pleroma.Formatter alias Pleroma.HTML - alias Pleroma.Web.MediaProxy def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do content # html content comes from DB already encoded, decode first and scrub after |> HtmlEntities.decode() |> String.replace(~r//, " ") - |> HTML.get_cached_stripped_html_for_activity(object, "metadata") + |> Activity.HTML.get_cached_stripped_html_for_activity(object, "metadata") |> Emoji.Formatter.demojify() |> HtmlEntities.decode() |> Formatter.truncate() @@ -37,10 +37,6 @@ def scrub_html(content) when is_binary(content) do def scrub_html(content), do: content - def attachment_url(url) do - MediaProxy.preview_url(url) - end - def user_name_string(user) do "#{user.name} " <> if user.local do diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex index 6a0112d2a..3781781c8 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex @@ -35,7 +35,9 @@ def get_nodeinfo("2.0") do openRegistrations: Config.get([:instance, :registrations_open]), usage: %{ users: %{ - total: Map.get(stats, :user_count, 0) + total: Map.get(stats, :user_count, 0), + activeMonth: Pleroma.User.active_user_count(30), + activeHalfyear: Pleroma.User.active_user_count(180) }, localPosts: Map.get(stats, :status_count, 0) }, diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index bca94d236..69ec27ba0 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do use Pleroma.Web, :controller - alias Pleroma.Web + alias Pleroma.Web.Endpoint alias Pleroma.Web.Nodeinfo.Nodeinfo def schemas(conn, _params) do @@ -13,11 +13,11 @@ def schemas(conn, _params) do links: [ %{ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", - href: Web.base_url() <> "/nodeinfo/2.0.json" + href: Endpoint.url() <> "/nodeinfo/2.0.json" }, %{ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1", - href: Web.base_url() <> "/nodeinfo/2.1.json" + href: Endpoint.url() <> "/nodeinfo/2.1.json" } ] } diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex index 215d97b3a..0d7d17b8a 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -12,8 +12,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web.Auth.Authenticator - alias Pleroma.Web.ControllerHelper + alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.MFAController @@ -24,6 +23,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken alias Pleroma.Web.Plugs.RateLimiter + alias Pleroma.Web.Utils.Params require Logger @@ -32,10 +32,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do plug(:fetch_session) plug(:fetch_flash) - plug(:skip_plug, [ - Pleroma.Web.Plugs.OAuthScopesPlug, - Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - ]) + plug(:skip_auth) plug(RateLimiter, [name: :authentication] when action == :create_authorization) @@ -50,7 +47,7 @@ def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do end def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do - if ControllerHelper.truthy_param?(params["force_login"]) do + if Params.truthy_param?(params["force_login"]) do do_authorize(conn, params) else handle_existing_authorization(conn, params) @@ -427,7 +424,7 @@ def prepare_request(%Plug.Conn{} = conn, %{ |> Map.put("state", state) # Handing the request to Ueberauth - redirect(conn, to: o_auth_path(conn, :request, provider, params)) + redirect(conn, to: Routes.o_auth_path(conn, :request, provider, params)) end def request(%Plug.Conn{} = conn, params) do @@ -600,9 +597,6 @@ def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested end end - # Special case: Local MastodonFE - defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login) - defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 165afd3b4..8e4d3e7f7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.RateLimiter @@ -29,10 +28,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug( - :skip_plug, - [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend - ) + plug(:skip_auth when action == :confirmation_resend) plug( OAuthScopesPlug, @@ -47,7 +43,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) - plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index dcd54b1af..669d50132 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -151,7 +151,9 @@ def index2(%{assigns: %{user: user}} = conn, params) do index_query(user, params) |> Pagination.fetch_paginated(params) - render(conn, "index.json", chats: chats) + conn + |> add_link_headers(chats) + |> render("index.json", chats: chats) end defp index_query(%{id: user_id} = user, params) do diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index d285e4907..be2f4617d 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -13,7 +13,6 @@ defmodule Pleroma.Web.PleromaAPI.ConversationController do alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) - plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses]) plug( diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex index d0f677d3c..1ea44f347 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -22,11 +22,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do ] ) - @skip_plugs [ - Pleroma.Web.Plugs.OAuthScopesPlug, - Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug - ] - plug(:skip_plug, @skip_plugs when action in [:index, :archive, :show]) + plug(:skip_auth when action in [:index, :archive, :show]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex index 257bcd550..bcb3a9ae1 100644 --- a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex @@ -14,8 +14,6 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do %{scopes: ["write:notifications"]} when action == :mark_as_read ) - plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView) - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do diff --git a/lib/pleroma/web/pleroma_api/views/account_view.ex b/lib/pleroma/web/pleroma_api/views/account_view.ex new file mode 100644 index 000000000..28941f471 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/account_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.AccountView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.AccountView.render(view, opts) +end diff --git a/lib/pleroma/web/pleroma_api/views/conversation_view.ex b/lib/pleroma/web/pleroma_api/views/conversation_view.ex new file mode 100644 index 000000000..173006360 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/conversation_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ConversationView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.ConversationView.render(view, opts) +end diff --git a/lib/pleroma/web/pleroma_api/views/notification_view.ex b/lib/pleroma/web/pleroma_api/views/notification_view.ex new file mode 100644 index 000000000..36b2fdfe8 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/notification_view.ex @@ -0,0 +1,10 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.NotificationView do + use Pleroma.Web, :view + alias Pleroma.Web.MastodonAPI + + def render(view, opts), do: MastodonAPI.NotificationView.render(view, opts) +end diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex index eb385e94d..ebe7eaf86 100644 --- a/lib/pleroma/web/plugs/frontend_static.ex +++ b/lib/pleroma/web/plugs/frontend_static.ex @@ -10,8 +10,6 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do """ @behaviour Plug - @api_routes Pleroma.Web.get_api_routes() - def file_path(path, frontend_type \\ :primary) do if configuration = Pleroma.Config.get([:frontends, frontend_type]) do instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") @@ -55,10 +53,13 @@ defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t) defp invalid_path?([], _match), do: false - defp api_route?([h | _]) when h in @api_routes, do: true - defp api_route?([_ | t]), do: api_route?(t) defp api_route?([]), do: false + defp api_route?([h | t]) do + api_routes = Pleroma.Web.Router.get_api_routes() + if h in api_routes, do: true, else: api_route?(t) + end + defp call_static(conn, opts, from) do opts = Map.put(opts, :from, from) Plug.Static.call(conn, opts) diff --git a/lib/pleroma/web/plugs/user_is_staff_plug.ex b/lib/pleroma/web/plugs/user_is_staff_plug.ex new file mode 100644 index 000000000..49c2d9cca --- /dev/null +++ b/lib/pleroma/web/plugs/user_is_staff_plug.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.UserIsStaffPlug do + import Pleroma.Web.TranslationHelpers + import Plug.Conn + + alias Pleroma.User + + def init(options) do + options + end + + def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _), do: conn + def call(%{assigns: %{user: %User{is_moderator: true}}} = conn, _), do: conn + + def call(conn, _) do + conn + |> render_error(:forbidden, "User is not a staff member.") + |> halt() + end +end diff --git a/lib/pleroma/web/preload.ex b/lib/pleroma/web/preload.ex index e8588bcc9..34a181e17 100644 --- a/lib/pleroma/web/preload.ex +++ b/lib/pleroma/web/preload.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.Preload do alias Phoenix.HTML - require Logger def build_tags(_conn, params) do preload_data = diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 83cbdc870..28e13ef9c 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -124,8 +124,8 @@ def build_content(notification, actor, object, mastodon_type) do def format_body(activity, actor, object, mastodon_type \\ nil) - def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do - case content do + def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do + case data["content"] do nil -> "@#{actor.nickname}: (Attachment)" content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" end diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 4f6c9bc9f..35bf2e223 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do end # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention pleroma:emoji_reaction]a + @supported_alert_types ~w[follow favourite mention reblog poll pleroma:chat_mention pleroma:emoji_reaction]a defp alerts(%{data: %{alerts: alerts}}) do alerts = Map.take(alerts, @supported_alert_types) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d41c4f7fe..e278036a2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Router do use Pleroma.Web, :router + import Phoenix.LiveDashboard.Router pipeline :accepts_html do plug(:accepts, ["html"]) @@ -96,14 +97,12 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug) plug(:after_auth) plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug) - plug(Pleroma.Web.Plugs.UserIsAdminPlug) + plug(Pleroma.Web.Plugs.UserIsStaffPlug) plug(Pleroma.Web.Plugs.IdempotencyPlug) end - pipeline :mastodon_html do - plug(:browser) - plug(:authenticate) - plug(:after_auth) + pipeline :require_admin do + plug(Pleroma.Web.Plugs.UserIsAdminPlug) end pipeline :pleroma_html do @@ -140,6 +139,10 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) end + pipeline :static_fe do + plug(Pleroma.Web.Plugs.StaticFEPlug) + end + scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:pleroma_api) @@ -148,6 +151,7 @@ defmodule Pleroma.Web.Router do get("/emoji", UtilController, :emoji) get("/captcha", UtilController, :captcha) get("/healthcheck", UtilController, :healthcheck) + post("/remote_interaction", UtilController, :remote_interaction) end scope "/api/v1/pleroma", Pleroma.Web do @@ -155,12 +159,11 @@ defmodule Pleroma.Web.Router do post("/uploader_callback/:upload_path", UploaderController, :callback) end + # AdminAPI: only admins can perform these actions scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do - pipe_through(:admin_api) + pipe_through([:admin_api, :require_admin]) put("/users/disable_mfa", AdminAPIController, :disable_mfa) - put("/users/tag", AdminAPIController, :tag_users) - delete("/users/tag", AdminAPIController, :untag_users) get("/users/:nickname/permission_group", AdminAPIController, :right_get) get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) @@ -183,16 +186,61 @@ defmodule Pleroma.Web.Router do post("/users/follow", UserController, :follow) post("/users/unfollow", UserController, :unfollow) - delete("/users", UserController, :delete) post("/users", UserController, :create) + + patch("/users/suggest", UserController, :suggest) + patch("/users/unsuggest", UserController, :unsuggest) + + get("/relay", RelayController, :index) + post("/relay", RelayController, :follow) + delete("/relay", RelayController, :unfollow) + + get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) + patch("/users/force_password_reset", AdminAPIController, :force_password_reset) + get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials) + patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) + + get("/instance_document/:name", InstanceDocumentController, :show) + patch("/instance_document/:name", InstanceDocumentController, :update) + delete("/instance_document/:name", InstanceDocumentController, :delete) + + patch("/users/confirm_email", AdminAPIController, :confirm_email) + patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) + + get("/config", ConfigController, :show) + post("/config", ConfigController, :update) + get("/config/descriptions", ConfigController, :descriptions) + get("/need_reboot", AdminAPIController, :need_reboot) + get("/restart", AdminAPIController, :restart) + + get("/oauth_app", OAuthAppController, :index) + post("/oauth_app", OAuthAppController, :create) + patch("/oauth_app/:id", OAuthAppController, :update) + delete("/oauth_app/:id", OAuthAppController, :delete) + + get("/media_proxy_caches", MediaProxyCacheController, :index) + post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) + post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) + + get("/frontends", FrontendController, :index) + post("/frontends/install", FrontendController, :install) + + post("/backups", AdminAPIController, :create_backup) + end + + # AdminAPI: admins and mods (staff) can perform these actions + scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do + pipe_through(:admin_api) + + put("/users/tag", AdminAPIController, :tag_users) + delete("/users/tag", AdminAPIController, :untag_users) + patch("/users/:nickname/toggle_activation", UserController, :toggle_activation) patch("/users/activate", UserController, :activate) patch("/users/deactivate", UserController, :deactivate) patch("/users/approve", UserController, :approve) - get("/relay", RelayController, :index) - post("/relay", RelayController, :follow) - delete("/relay", RelayController, :unfollow) + delete("/users", UserController, :delete) post("/users/invite_token", InviteController, :create) get("/users/invites", InviteController, :index) @@ -209,14 +257,8 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/chats", AdminAPIController, :list_user_chats) - get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses) - - get("/instance_document/:name", InstanceDocumentController, :show) - patch("/instance_document/:name", InstanceDocumentController, :update) - delete("/instance_document/:name", InstanceDocumentController, :delete) - - patch("/users/confirm_email", AdminAPIController, :confirm_email) - patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email) + get("/instances/:instance/statuses", InstanceController, :list_statuses) + delete("/instances/:instance", InstanceController, :delete) get("/reports", ReportController, :index) get("/reports/:id", ReportController, :show) @@ -229,34 +271,14 @@ defmodule Pleroma.Web.Router do delete("/statuses/:id", StatusController, :delete) get("/statuses", StatusController, :index) - get("/config", ConfigController, :show) - post("/config", ConfigController, :update) - get("/config/descriptions", ConfigController, :descriptions) - get("/need_reboot", AdminAPIController, :need_reboot) - get("/restart", AdminAPIController, :restart) - get("/moderation_log", AdminAPIController, :list_log) post("/reload_emoji", AdminAPIController, :reload_emoji) get("/stats", AdminAPIController, :stats) - get("/oauth_app", OAuthAppController, :index) - post("/oauth_app", OAuthAppController, :create) - patch("/oauth_app/:id", OAuthAppController, :update) - delete("/oauth_app/:id", OAuthAppController, :delete) - - get("/media_proxy_caches", MediaProxyCacheController, :index) - post("/media_proxy_caches/delete", MediaProxyCacheController, :delete) - post("/media_proxy_caches/purge", MediaProxyCacheController, :purge) - get("/chats/:id", ChatController, :show) get("/chats/:id/messages", ChatController, :messages) delete("/chats/:id/messages/:message_id", ChatController, :delete_message) - - get("/frontends", FrontendController, :index) - post("/frontends/install", FrontendController, :install) - - post("/backups", AdminAPIController, :create_backup) end scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do @@ -453,6 +475,7 @@ defmodule Pleroma.Web.Router do post("/accounts/:id/unblock", AccountController, :unblock) post("/accounts/:id/mute", AccountController, :mute) post("/accounts/:id/unmute", AccountController, :unmute) + post("/accounts/:id/note", AccountController, :note) get("/conversations", ConversationController, :index) post("/conversations/:id/read", ConversationController, :mark_as_read) @@ -532,19 +555,13 @@ defmodule Pleroma.Web.Router do delete("/push/subscription", SubscriptionController, :delete) get("/suggestions", SuggestionController, :index) + delete("/suggestions/:account_id", SuggestionController, :dismiss) get("/timelines/home", TimelineController, :home) get("/timelines/direct", TimelineController, :direct) get("/timelines/list/:list_id", TimelineController, :list) end - scope "/api/web", Pleroma.Web do - pipe_through(:authenticated_api) - - # Backend-obscure settings blob for MastoFE, don't parse/reuse elsewhere - put("/settings", MastoFEController, :put_settings) - end - scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:app_api) @@ -590,6 +607,8 @@ defmodule Pleroma.Web.Router do get("/search", SearchController, :search2) post("/media", MediaController, :create2) + + get("/suggestions", SuggestionController, :index2) end scope "/api", Pleroma.Web do @@ -620,18 +639,12 @@ defmodule Pleroma.Web.Router do get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens) delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token) - - post( - "/qvitter/statuses/notifications/read", - TwitterAPI.Controller, - :mark_notifications_as_read - ) end scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled # Note: http signature is only considered for json requests (no auth for non-json requests) - pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug]) + pipe_through([:accepts_html_json, :http_signature, :static_fe]) get("/objects/:uuid", OStatus.OStatusController, :object) get("/activities/:uuid", OStatus.OStatusController, :activity) @@ -650,7 +663,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled # Note: http signature is only considered for json requests (no auth for non-json requests) - pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug]) + pipe_through([:accepts_html_xml_json, :http_signature, :static_fe]) # Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) @@ -658,7 +671,7 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do # Note: html format is supported only if static FE is enabled - pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug]) + pipe_through([:accepts_html_xml, :static_fe]) get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) end @@ -754,27 +767,20 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do pipe_through(:api) - get("/web/manifest.json", MastoFEController, :manifest) + get("/manifest.json", ManifestController, :show) end scope "/", Pleroma.Web do - pipe_through(:mastodon_html) + pipe_through(:pleroma_html) - get("/web/login", MastodonAPI.AuthController, :login) - delete("/auth/sign_out", MastodonAPI.AuthController, :logout) - - post("/auth/password", MastodonAPI.AuthController, :password_reset) - - get("/web/*path", MastoFEController, :index) - - get("/embed/:id", EmbedController, :show) + post("/auth/password", TwitterAPI.PasswordController, :request) end - scope "/proxy/", Pleroma.Web.MediaProxy do - get("/preview/:sig/:url", MediaProxyController, :preview) - get("/preview/:sig/:url/:filename", MediaProxyController, :preview) - get("/:sig/:url", MediaProxyController, :remote) - get("/:sig/:url/:filename", MediaProxyController, :remote) + scope "/proxy/", Pleroma.Web do + get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview) + get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview) + get("/:sig/:url", MediaProxy.MediaProxyController, :remote) + get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote) end if Pleroma.Config.get(:env) == :dev do @@ -785,6 +791,11 @@ defmodule Pleroma.Web.Router do end end + scope "/" do + pipe_through([:pleroma_html, :authenticate, :require_admin]) + live_dashboard("/phoenix/live_dashboard") + end + # Test-only routes needed to test action dispatching and plug chain execution if Pleroma.Config.get(:env) == :test do @test_actions [ @@ -827,4 +838,16 @@ defmodule Pleroma.Web.Router do options("/*path", RedirectController, :empty) end + + # TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+ + def get_api_routes do + __MODULE__.__routes__() + |> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end) + |> Enum.map(fn r -> + r.path + |> String.split("/", trim: true) + |> List.first() + end) + |> Enum.uniq() + end end diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/shout_channel.ex similarity index 78% rename from lib/pleroma/web/chat_channel.ex rename to lib/pleroma/web/shout_channel.ex index 4008129e9..17caecb1a 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/shout_channel.ex @@ -2,12 +2,12 @@ # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.ChatChannel do +defmodule Pleroma.Web.ShoutChannel do use Phoenix.Channel alias Pleroma.User - alias Pleroma.Web.ChatChannel.ChatChannelState alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.ShoutChannel.ShoutChannelState def join("chat:public", _message, socket) do send(self(), :after_join) @@ -15,18 +15,18 @@ def join("chat:public", _message, socket) do end def handle_info(:after_join, socket) do - push(socket, "messages", %{messages: ChatChannelState.messages()}) + push(socket, "messages", %{messages: ShoutChannelState.messages()}) {:noreply, socket} end def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do text = String.trim(text) - if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do + if String.length(text) in 1..Pleroma.Config.get([:shout, :limit]) do author = User.get_cached_by_nickname(user_name) author_json = AccountView.render("show.json", user: author, skip_visibility_check: true) - message = ChatChannelState.add_message(%{text: text, author: author_json}) + message = ShoutChannelState.add_message(%{text: text, author: author_json}) broadcast!(socket, "new_msg", message) end @@ -35,7 +35,7 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} end end -defmodule Pleroma.Web.ChatChannel.ChatChannelState do +defmodule Pleroma.Web.ShoutChannel.ShoutChannelState do use Agent @max_messages 20 diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 421070636..827c0a384 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do alias Pleroma.Web.Router.Helpers plug(:put_layout, :static_fe) - plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:assign_id) @page_keys ["max_id", "min_id", "limit", "since_id", "order"] diff --git a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex index 6688830ba..57bd92468 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.atom.eex @@ -38,7 +38,7 @@ <%= if id == Pleroma.Constants.as_public() do %> <% else %> - <%= unless Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) do %> + <%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %> <% end %> <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex index 592b9dcdc..279f2171d 100644 --- a/lib/pleroma/web/templates/feed/feed/_activity.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/_activity.rss.eex @@ -38,7 +38,7 @@ <%= if id == Pleroma.Constants.as_public() do %> http://activityschema.org/collection/public <% else %> - <%= unless Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) do %> + <%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %> <%= id %> <% end %> <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex index c2de28fe4..aa3035bca 100644 --- a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex @@ -33,7 +33,7 @@ ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> <% else %> - <%= unless Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) do %> + <%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %> diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex index a288539ed..de0731085 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex @@ -9,13 +9,13 @@ xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/"> - <%= '#{tag_feed_url(@conn, :feed, @tag)}.rss' %> + <%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> #<%= @tag %> These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse. <%= feed_logo() %> <%= most_recent_update(@activities) %> - + <%= for activity <- @activities do %> <%= render @view_module, "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %> <% end %> diff --git a/lib/pleroma/web/templates/feed/feed/tag.rss.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex index eeda01a04..9c3613feb 100644 --- a/lib/pleroma/web/templates/feed/feed/tag.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/tag.rss.eex @@ -5,7 +5,7 @@ #<%= @tag %> These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse. - <%= '#{tag_feed_url(@conn, :feed, @tag)}.rss' %> + <%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %> <%= feed_logo() %> 2b90d9 <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/feed/feed/user.atom.eex b/lib/pleroma/web/templates/feed/feed/user.atom.eex index c6acd848f..5c1f0ecbc 100644 --- a/lib/pleroma/web/templates/feed/feed/user.atom.eex +++ b/lib/pleroma/web/templates/feed/feed/user.atom.eex @@ -6,16 +6,16 @@ xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0"> - <%= user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %> + <%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %> <%= @user.nickname <> "'s timeline" %> <%= most_recent_update(@activities, @user) %> <%= logo(@user) %> - + <%= render @view_module, "_author.atom", assigns %> <%= if last_activity(@activities) do %> - + <% end %> <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/feed/feed/user.rss.eex b/lib/pleroma/web/templates/feed/feed/user.rss.eex index d69120480..6b842a085 100644 --- a/lib/pleroma/web/templates/feed/feed/user.rss.eex +++ b/lib/pleroma/web/templates/feed/feed/user.rss.eex @@ -1,16 +1,16 @@ - <%= user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %> + <%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %> <%= @user.nickname <> "'s timeline" %> <%= most_recent_update(@activities, @user) %> <%= logo(@user) %> - <%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss' %> + <%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %> <%= render @view_module, "_author.rss", assigns %> <%= if last_activity(@activities) do %> - <%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %> + <%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %> <% end %> <%= for activity <- @activities do %> diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/masto_fe/index.html.eex deleted file mode 100644 index c330960fa..000000000 --- a/lib/pleroma/web/templates/masto_fe/index.html.eex +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - -<%= Config.get([:instance, :name]) %> - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - diff --git a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex index 5ab59b57b..b9daa8d8b 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex @@ -7,7 +7,7 @@

Two-factor recovery

-<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %> +<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<%= label f, :code, "Recovery code" %> <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %> @@ -19,6 +19,6 @@ <%= submit "Verify" %> <% end %> -"> +"> Enter a two-factor code diff --git a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex index af85777eb..29ea7c5fb 100644 --- a/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex +++ b/lib/pleroma/web/templates/o_auth/mfa/totp.html.eex @@ -7,7 +7,7 @@

Two-factor authentication

-<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %> +<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
<%= label f, :code, "Authentication code" %> <%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %> @@ -19,6 +19,6 @@ <%= submit "Verify" %> <% end %> -"> +"> Enter a two-factor recovery code diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 4a0718851..dc4521a62 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,6 +1,6 @@

Sign in with external provider

-<%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> +<%= form_for @conn, Routes.o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex index facedc8db..99f900fb7 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/register.html.eex @@ -8,7 +8,7 @@

Registration Details

If you'd like to register a new account, please provide the details below.

-<%= form_for @conn, o_auth_path(@conn, :register), [as: "authorization"], fn f -> %> +<%= form_for @conn, Routes.o_auth_path(@conn, :register), [as: "authorization"], fn f -> %>
<%= label f, :nickname, "Nickname" %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 1a85818ec..181a9519a 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -5,7 +5,7 @@ <% end %> -<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> +<%= form_for @conn, Routes.o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> <%= if @user do %>