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 2bc571971..b155c81bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,9 @@ variables: &global_variables MIX_ENV: test cache: &global_cache_policy - key: ${CI_COMMIT_REF_SLUG} + key: + files: + - mix.lock paths: - deps - _build @@ -22,6 +24,7 @@ stages: - docker before_script: + - rm -rf _build/*/lib/pleroma - apt-get update && apt-get install -y cmake - mix local.hex --force - mix local.rebar --force @@ -29,13 +32,25 @@ before_script: - apt-get -qq update - apt-get install -y libmagic-dev +after_script: + - rm -rf _build/*/lib/pleroma + 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 @@ -58,6 +73,11 @@ benchmark: unit-testing: stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" retry: 2 cache: &testing_cache_policy <<: *global_cache_policy @@ -91,6 +111,11 @@ unit-testing: unit-testing-rum: stage: test + only: + changes: + - "**/*.ex" + - "**/*.exs" + - "mix.lock" retry: 2 cache: *testing_cache_policy services: @@ -109,12 +134,22 @@ unit-testing-rum: lint: 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 @@ -171,8 +206,8 @@ spec-deploy: - apk add curl script: - curl -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline - - + + stop_review_app: image: alpine:3.9 stage: deploy @@ -231,7 +266,7 @@ amd64-musl: stage: release artifacts: *release-artifacts only: *release-only - image: elixir:1.10.3-alpine + image: elixir:1.10.3-alpine cache: *release-cache variables: *release-variables before_script: &before-release-musl diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f590dbd3..72a67eb8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +- **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit` - 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. ### 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`. +- `AnalyzeMetadata` upload filter for extracting attachment dimensions and generating blurhashes. +- Attachment dimensions and blurhashes are federated when available. +- Pinned posts federation + +### Fixed +- Don't crash so hard when email settings are invalid. +- Checking activated Upload Filters for required commands. + +### Removed +- **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`) ## Unreleased (Patch) @@ -26,6 +40,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 diff --git a/Dockerfile b/Dockerfile index b1b5171af..db1a6b457 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ - apk add exiftool imagemagick libmagic ncurses postgresql-client &&\ + 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..6aa36d89a 100644 --- a/README.md +++ b/README.md @@ -50,5 +50,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..b9f6b24da 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" } diff --git a/config/benchmark.exs b/config/benchmark.exs index 5567ff26e..a4d048f1b 100644 --- a/config/benchmark.exs +++ b/config/benchmark.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. diff --git a/config/config.exs b/config/config.exs index 07565c557..c00d5dca9 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] @@ -190,7 +190,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, @@ -454,7 +453,9 @@ 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 diff --git a/config/description.exs b/config/description.exs index bc3f11dde..94907aaba 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, @@ -1182,7 +1175,6 @@ alwaysShowSubjectInput: true, background: "/static/aurora_borealis.jpg", collapseMessageWithSubject: false, - disableChat: false, greentext: false, hideFilteredStatuses: false, hideMutedPosts: false, @@ -1229,12 +1221,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", @@ -2633,13 +2619,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..6b7ffb0e9 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,6 +54,10 @@ 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 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 5c3313a2c..59061aaa6 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,7 +37,7 @@ 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. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index a14fcb416..6c1ecb559 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -38,6 +38,7 @@ Has these additional fields under the `pleroma` object: - `thread_muted`: true if the thread the post belongs to is muted - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. - `parent_visible`: If the parent of this post is visible to the user or not. +- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise. ## Scheduled statuses @@ -255,9 +256,29 @@ This information is returned in the `/api/v1/accounts/verify_credentials` endpoi *Pleroma supports refreshing tokens.* -`POST /oauth/token` +### POST `/oauth/token` -Post here request with `grant_type=refresh_token` to obtain new access token. Returns an access token. +You can obtain access tokens for a user in a few additional ways. + +#### Refreshing a token + +To obtain a new access token from a refresh token, pass `grant_type=refresh_token` with the following extra parameters: + +- `refresh_token`: The refresh token. + +#### Getting a token with a password + +To obtain a token from a user's password, pass `grant_type=password` with the following extra parameters: + +- `username`: Username to authenticate. +- `password`: The user's password. + +#### Response body + +Additional fields are returned in the response: + +- `id`: The primary key of this token in Pleroma's database. +- `me` (user tokens only): The ActivityPub ID of the user who owns the token. ## Account Registration diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 7eb1718f2..54859bf03 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -117,7 +117,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 +240,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new , 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 @@ -290,7 +290,7 @@ nginx -t ## Create your first user and set as admin ```sh -cd /opt/pleroma/bin +cd /opt/pleroma su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin" ``` This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password. @@ -301,4 +301,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/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index d59403884..53beca5e6 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -184,40 +184,48 @@ def get_by_ap_id_with_object(ap_id) do |> Repo.one() end - @spec get_by_id(String.t()) :: Activity.t() | nil - def get_by_id(id) do - case FlakeId.flake_id?(id) do - true -> - Activity - |> where([a], a.id == ^id) - |> restrict_deactivated_users() - |> Repo.one() + @doc """ + Gets activity by ID, doesn't load activities from deactivated actors by default. + """ + @spec get_by_id(String.t(), keyword()) :: t() | nil + def get_by_id(id, opts \\ [filter: [:restrict_deactivated]]), do: get_by_id_with_opts(id, opts) - _ -> - nil + @spec get_by_id_with_user_actor(String.t()) :: t() | nil + def get_by_id_with_user_actor(id), do: get_by_id_with_opts(id, preload: [:user_actor]) + + @spec get_by_id_with_object(String.t()) :: t() | nil + def get_by_id_with_object(id), do: get_by_id_with_opts(id, preload: [:object]) + + defp get_by_id_with_opts(id, opts) do + if FlakeId.flake_id?(id) do + query = Queries.by_id(id) + + with_filters_query = + if is_list(opts[:filter]) do + Enum.reduce(opts[:filter], query, fn + {:type, type}, acc -> Queries.by_type(acc, type) + :restrict_deactivated, acc -> restrict_deactivated_users(acc) + _, acc -> acc + end) + else + query + end + + with_preloads_query = + if is_list(opts[:preload]) do + Enum.reduce(opts[:preload], with_filters_query, fn + :user_actor, acc -> with_preloaded_user_actor(acc) + :object, acc -> with_preloaded_object(acc) + _, acc -> acc + end) + else + with_filters_query + end + + Repo.one(with_preloads_query) end end - def get_by_id_with_user_actor(id) do - case FlakeId.flake_id?(id) do - true -> - Activity - |> where([a], a.id == ^id) - |> with_preloaded_user_actor() - |> Repo.one() - - _ -> - nil - end - end - - def get_by_id_with_object(id) do - Activity - |> where(id: ^id) - |> with_preloaded_object() - |> Repo.one() - end - def all_by_ids_with_object(ids) do Activity |> where([a], a.id in ^ids) @@ -269,6 +277,11 @@ def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do def get_create_by_object_ap_id_with_object(_), do: nil + @spec create_by_id_with_object(String.t()) :: t() | nil + def create_by_id_with_object(id) do + get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"]) + end + defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do get_create_by_object_ap_id_with_object(ap_id) end @@ -368,12 +381,6 @@ def direct_conversation_id(activity, for_user) do end end - @spec pinned_by_actor?(Activity.t()) :: boolean() - def pinned_by_actor?(%Activity{} = activity) do - actor = user_actor(activity) - activity.id in actor.pinned_activities - end - @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do ap_id @@ -384,4 +391,13 @@ def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do end def get_by_object_ap_id_with_object(_), do: nil + + @spec add_by_params_query(String.t(), String.t(), String.t()) :: Ecto.Query.t() + def add_by_params_query(object_id, actor, target) do + object_id + |> Queries.by_object_id() + |> Queries.by_type("Add") + |> Queries.by_actor(actor) + |> where([a], fragment("?->>'target' = ?", a.data, ^target)) + end end 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/queries.ex b/lib/pleroma/activity/queries.ex index a6b02a889..4632651b0 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -14,6 +14,11 @@ defmodule Pleroma.Activity.Queries do alias Pleroma.Activity alias Pleroma.User + @spec by_id(query(), String.t()) :: query() + def by_id(query \\ Activity, id) do + from(a in query, where: a.id == ^id) + end + @spec by_ap_id(query, String.t()) :: query def by_ap_id(query \\ Activity, ap_id) do from( 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 6ef65b263..ee6ee9516 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -34,15 +34,16 @@ defp handle_result({:error, message}), do: raise(VerifyError, message: message) defp check_welcome_message_config!(:ok) do if Pleroma.Config.get([:welcome, :email, :enabled], false) and not Pleroma.Emails.Mailer.enabled?() do - Logger.error(""" - To send welcome email do you need to enable mail. - \nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true - """) + Logger.warn(""" + To send welcome emails, you need to enable the mailer. + Welcome emails will NOT be sent with the current config. - {:error, "The mail disabled."} - else - :ok + Enable the mailer: + config :pleroma, Pleroma.Emails.Mailer, enabled: true + """) end + + :ok end defp check_welcome_message_config!(result), do: result @@ -51,18 +52,21 @@ defp check_welcome_message_config!(result), do: result # def check_confirmation_accounts!(:ok) do if Pleroma.Config.get([:instance, :account_activation_required]) && - not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do - Logger.error( - "Account activation enabled, but no Mailer settings enabled.\n" <> - "Please set config :pleroma, :instance, account_activation_required: false\n" <> - "Otherwise setup and enable Mailer." - ) + not Pleroma.Emails.Mailer.enabled?() do + Logger.warn(""" + Account activation is required, but the mailer is disabled. + Users will NOT be able to confirm their accounts with this config. + Either disable account activation or enable the mailer. - {:error, - "Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."} - else - :ok + Disable account activation: + config :pleroma, :instance, account_activation_required: false + + Enable the mailer: + config :pleroma, Pleroma.Emails.Mailer, enabled: true + """) end + + :ok end def check_confirmation_accounts!(result), do: result @@ -160,9 +164,11 @@ 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") ] preview_proxy_commands_status = diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex index 24aa5993b..fedd58a7e 100644 --- a/lib/pleroma/config/deprecation_warnings.ex +++ b/lib/pleroma/config/deprecation_warnings.ex @@ -41,7 +41,8 @@ def warn do :ok <- check_gun_pool_options(), :ok <- check_activity_expiration_config(), :ok <- check_remote_ip_plug_name(), - :ok <- check_uploders_s3_public_endpoint() do + :ok <- check_uploders_s3_public_endpoint(), + :ok <- check_old_chat_shoutbox() do :ok else _ -> @@ -215,4 +216,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..9489f58c4 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -3,19 +3,21 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.Loader do - @reject_keys [ - Pleroma.Repo, - Pleroma.Web.Endpoint, - :env, - :configurable_from_database, - :database, - :swarm - ] + defp reject_keys, + do: [ + Pleroma.Repo, + Pleroma.Web.Endpoint, + :env, + :configurable_from_database, + :database, + :swarm + ] - @reject_groups [ - :postgrex, - :tesla - ] + defp reject_groups, + do: [ + :postgrex, + :tesla + ] if Code.ensure_loaded?(Config.Reader) do @reader Config.Reader @@ -52,7 +54,7 @@ defp filter(configs) do @spec filter_group(atom(), keyword()) :: keyword() def filter_group(group, configs) do Enum.reject(configs[group], fn {key, _v} -> - key in @reject_keys or group in @reject_groups or + key in reject_keys() or group in reject_groups() or (group == :phoenix and key == :serve_endpoints) end) end diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex index 8227195dc..e5e9d3dcd 100644 --- a/lib/pleroma/config/release_runtime_provider.ex +++ b/lib/pleroma/config/release_runtime_provider.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do @moduledoc """ - Imports `runtime.exs` and `{env}.exported_from_db.secret.exs` for elixir releases. + Imports runtime config and `{env}.exported_from_db.secret.exs` for releases. """ @behaviour Config.Provider @@ -8,10 +8,11 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do def init(opts), do: opts @impl true - def load(config, _opts) do + def load(config, opts) do with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults()) - config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" + config_path = + opts[:config_path] || System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" with_runtime_config = if File.exists?(config_path) do @@ -24,7 +25,7 @@ def load(config, _opts) do warning = [ IO.ANSI.red(), IO.ANSI.bright(), - "!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file", + "!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file", IO.ANSI.reset() ] @@ -33,13 +34,14 @@ def load(config, _opts) do end exported_config_path = - config_path - |> Path.dirname() - |> Path.join("prod.exported_from_db.secret.exs") + opts[:exported_config_path] || + config_path + |> Path.dirname() + |> Path.join("#{Pleroma.Config.get(:env)}.exported_from_db.secret.exs") with_exported = if File.exists?(exported_config_path) do - exported_config = Config.Reader.read!(with_runtime_config) + exported_config = Config.Reader.read!(exported_config_path) Config.Reader.merge(with_runtime_config, exported_config) else with_runtime_config diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index aad45aab8..d5c6081a2 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?) @@ -165,12 +167,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/earmark_renderer.ex b/lib/pleroma/earmark_renderer.ex deleted file mode 100644 index 31cae3c72..000000000 --- a/lib/pleroma/earmark_renderer.ex +++ /dev/null @@ -1,256 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2021 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only -# -# This file is derived from Earmark, under the following copyright: -# Copyright © 2014 Dave Thomas, The Pragmatic Programmers -# SPDX-License-Identifier: Apache-2.0 -# Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex -defmodule Pleroma.EarmarkRenderer do - @moduledoc false - - alias Earmark.Block - alias Earmark.Context - alias Earmark.HtmlRenderer - alias Earmark.Options - - import Earmark.Inline, only: [convert: 3] - import Earmark.Helpers.HtmlHelpers - import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2] - import Earmark.Context, only: [append: 2, set_value: 2] - import Earmark.Options, only: [get_mapper: 1] - - @doc false - def render(blocks, %Context{options: %Options{}} = context) do - messages = get_messages(context) - - {contexts, html} = - get_mapper(context.options).( - blocks, - &render_block(&1, put_in(context.options.messages, [])) - ) - |> Enum.unzip() - - all_messages = - contexts - |> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end) - - {put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()} - end - - ############# - # Paragraph # - ############# - defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do - lines = convert(lines, lnb, context) - add_attrs(lines, "

#{lines.value}

", attrs, [], lnb) - end - - ######## - # Html # - ######## - defp render_block(%Block.Html{html: html}, context) do - {context, html} - end - - defp render_block(%Block.HtmlComment{lines: lines}, context) do - {context, lines} - end - - defp render_block(%Block.HtmlOneline{html: html}, context) do - {context, html} - end - - ######### - # Ruler # - ######### - defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do - add_attrs(context, "
", attrs, [], lnb) - end - - ########### - # Heading # - ########### - defp render_block( - %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs}, - context - ) do - converted = convert(content, lnb, context) - html = "#{converted.value}" - add_attrs(converted, html, attrs, [], lnb) - end - - ############## - # Blockquote # - ############## - - defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do - {context1, body} = render(blocks, context) - html = "
#{body}
" - add_attrs(context1, html, attrs, [], lnb) - end - - ######### - # Table # - ######### - - defp render_block( - %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs}, - context - ) do - {context1, html} = add_attrs(context, "", attrs, [], lnb) - context2 = set_value(context1, html) - - context3 = - if header do - append(add_trs(append(context2, ""), [header], "th", aligns, lnb), "") - else - # Maybe an error, needed append(context, html) - context2 - end - - context4 = append(add_trs(append(context3, ""), rows, "td", aligns, lnb), "") - - {context4, [context4.value, "
"]} - end - - ######## - # Code # - ######## - - defp render_block( - %Block.Code{lnb: lnb, language: language, attrs: attrs} = block, - %Context{options: options} = context - ) do - class = - if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: "" - - tag = ~s[
]
-    lines = options.render_code.(block)
-    html = ~s[#{tag}#{lines}
] - add_attrs(context, html, attrs, [], lnb) - end - - ######### - # Lists # - ######### - - defp render_block( - %Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start}, - context - ) do - {context1, content} = render(items, context) - html = "<#{type}#{start}>#{content}" - add_attrs(context1, html, attrs, [], lnb) - end - - # format a single paragraph list item, and remove the para tags - defp render_block( - %Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs}, - context - ) - when length(blocks) == 1 do - {context1, content} = render(blocks, context) - content = Regex.replace(~r{}, content, "") - html = "
  • #{content}
  • " - add_attrs(context1, html, attrs, [], lnb) - end - - # format a spaced list item - defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do - {context1, content} = render(blocks, context) - html = "
  • #{content}
  • " - add_attrs(context1, html, attrs, [], lnb) - end - - ################## - # Footnote Block # - ################## - - defp render_block(%Block.FnList{blocks: footnotes}, context) do - items = - Enum.map(footnotes, fn note -> - blocks = append_footnote_link(note) - %Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks} - end) - - {context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context) - {context1, Enum.join([~s[
    ], "
    ", html, "
    "])} - end - - ####################################### - # Isolated IALs are rendered as paras # - ####################################### - - defp render_block(%Block.Ial{verbatim: verbatim}, context) do - {context, "

    {:#{verbatim}}

    "} - end - - #################### - # IDDef is ignored # - #################### - - defp render_block(%Block.IdDef{}, context), do: {context, ""} - - ##################################### - # And here are the inline renderers # - ##################################### - - defdelegate br, to: HtmlRenderer - defdelegate codespan(text), to: HtmlRenderer - defdelegate em(text), to: HtmlRenderer - defdelegate strong(text), to: HtmlRenderer - defdelegate strikethrough(text), to: HtmlRenderer - - defdelegate link(url, text), to: HtmlRenderer - defdelegate link(url, text, title), to: HtmlRenderer - - defdelegate image(path, alt, title), to: HtmlRenderer - - defdelegate footnote_link(ref, backref, number), to: HtmlRenderer - - # Table rows - defp add_trs(context, rows, tag, aligns, lnb) do - numbered_rows = - rows - |> Enum.zip(Stream.iterate(lnb, &(&1 + 1))) - - numbered_rows - |> Enum.reduce(context, fn {row, lnb}, ctx -> - append(add_tds(append(ctx, ""), row, tag, aligns, lnb), "") - end) - end - - defp add_tds(context, row, tag, aligns, lnb) do - Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb)) - end - - defp add_td_fn(row, tag, aligns, lnb) do - fn n, ctx -> - style = - case Enum.at(aligns, n - 1, :default) do - :default -> "" - align -> " style=\"text-align: #{align}\"" - end - - col = Enum.at(row, n - 1) - converted = convert(col, lnb, set_messages(ctx, [])) - append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}") - end - end - - ############################### - # Append Footnote Return Link # - ############################### - - defdelegate append_footnote_link(note), to: HtmlRenderer - defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer - - defdelegate render_code(lines), to: HtmlRenderer - - defp code_classes(language, prefix) do - ["" | String.split(prefix || "")] - |> Enum.map(fn pfx -> "#{pfx}#{language}" end) - |> Enum.join(" ") - end -end 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/formatter.ex b/lib/pleroma/formatter.ex index 7a08e48a9..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, @@ -121,6 +121,10 @@ def mentions_escape(text, options \\ []) do end end + def markdown_to_html(text) do + Earmark.as_html!(text, %Earmark.Options{compact_output: true}) + end + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end 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/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/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/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/containment.ex b/lib/pleroma/object/containment.ex index fb0398f92..040537acf 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -71,6 +71,14 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(oth compare_uris(id_uri, other_uri) end + # Mastodon pin activities don't have an id, so we check the object field, which will be pinned. + def contain_origin_from_id(id, %{"object" => object}) when is_binary(object) do + id_uri = URI.parse(id) + object_uri = URI.parse(object) + + compare_uris(id_uri, object_uri) + end + def contain_origin_from_id(_id, _data), do: :error def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), 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/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/analyze_metadata.ex b/lib/pleroma/upload/filter/analyze_metadata.ex new file mode 100644 index 000000000..8c23076d4 --- /dev/null +++ b/lib/pleroma/upload/filter/analyze_metadata.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.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(_), do: {:ok, :noop} + + defp get_blurhash(file) do + with {:ok, blurhash} <- :eblurhash.magick(file) do + blurhash + else + _ -> nil + 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 a3d41fcd0..46bac283c 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 @@ -99,6 +99,7 @@ defmodule Pleroma.User do field(:local, :boolean, default: true) field(:follower_address, :string) field(:following_address, :string) + field(:featured_address, :string) field(:search_rank, :float, virtual: true) field(:search_type, :integer, virtual: true) field(:tags, {:array, :string}, default: []) @@ -129,7 +130,6 @@ defmodule Pleroma.User do field(:hide_followers, :boolean, default: false) field(:hide_follows, :boolean, default: false) field(:hide_favorites, :boolean, default: true) - field(:pinned_activities, {:array, :string}, default: []) field(:email_notifications, :map, default: %{"digest" => false}) field(:mascot, :map, default: nil) field(:emoji, :map, default: %{}) @@ -147,6 +147,7 @@ defmodule Pleroma.User do field(:accepts_chat_messages, :boolean, default: nil) field(:last_active_at, :naive_datetime) field(:disclose_client, :boolean, default: true) + field(:pinned_objects, :map, default: %{}) embeds_one( :notification_settings, @@ -358,7 +359,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 @@ -366,13 +367,15 @@ 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 - def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" + @spec ap_id(User.t()) :: String.t() + 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 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" @@ -380,6 +383,11 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{} = user), do: "#{ap_id(user)}/following" + @spec ap_featured_collection(User.t()) :: String.t() + def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa + + def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured" + defp truncate_fields_param(params) do if Map.has_key?(params, :fields) do Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) @@ -442,6 +450,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :uri, :follower_address, :following_address, + :featured_address, :hide_followers, :hide_follows, :hide_followers_count, @@ -453,7 +462,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do :invisible, :actor_type, :also_known_as, - :accepts_chat_messages + :accepts_chat_messages, + :pinned_objects ] ) |> cast(params, [:name], empty_values: []) @@ -685,7 +695,7 @@ def register_changeset_ldap(struct, params = %{password: password}) |> validate_format(:nickname, local_nickname_regex()) |> put_ap_id() |> unique_constraint(:ap_id) - |> put_following_and_follower_address() + |> put_following_and_follower_and_featured_address() end def register_changeset(struct, params \\ %{}, opts \\ []) do @@ -746,7 +756,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> put_password_hash |> put_ap_id() |> unique_constraint(:ap_id) - |> put_following_and_follower_address() + |> put_following_and_follower_and_featured_address() end def maybe_validate_required_email(changeset, true), do: changeset @@ -764,11 +774,16 @@ defp put_ap_id(changeset) do put_change(changeset, :ap_id, ap_id) end - defp put_following_and_follower_address(changeset) do - followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) + defp put_following_and_follower_and_featured_address(changeset) do + user = %User{nickname: get_field(changeset, :nickname)} + followers = ap_followers(user) + following = ap_following(user) + featured = ap_featured_collection(user) changeset |> put_change(:follower_address, followers) + |> put_change(:following_address, following) + |> put_change(:featured_address, featured) end defp autofollow_users(user) do @@ -2334,45 +2349,35 @@ def approval_changeset(user, set_approval: approved?) do cast(user, %{is_approved: approved?}, [:is_approved]) end - def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do - if id not in user.pinned_activities do - max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) - params = %{pinned_activities: user.pinned_activities ++ [id]} - - # if pinned activity was scheduled for deletion, we remove job - if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do - Oban.cancel_job(expiration.id) - end + @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()} + def add_pinned_object_id(%User{} = user, object_id) do + if !user.pinned_objects[object_id] do + params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())} user - |> cast(params, [:pinned_activities]) - |> validate_length(:pinned_activities, - max: max_pinned_statuses, - message: "You have already pinned the maximum number of statuses" - ) + |> cast(params, [:pinned_objects]) + |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects -> + max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) + + if Enum.count(pinned_objects) <= max_pinned_statuses do + [] + else + [pinned_objects: "You have already pinned the maximum number of statuses"] + end + end) else change(user) end |> update_and_set_cache() end - def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do - params = %{pinned_activities: List.delete(user.pinned_activities, id)} - - # if pinned activity was scheduled for deletion, we reschedule it for deletion - if data["expires_at"] do - # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation - {:ok, expires_at} = - data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast() - - Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ - activity_id: id, - expires_at: expires_at - }) - end - + @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()} + def remove_pinned_object_id(%User{} = user, object_id) do user - |> cast(params, [:pinned_activities]) + |> cast( + %{pinned_objects: Map.delete(user.pinned_objects, object_id)}, + [:pinned_objects] + ) |> update_and_set_cache() end diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex index bc0c95332..a446d3ae6 100644 --- a/lib/pleroma/utils.ex +++ b/lib/pleroma/utils.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Utils do eperm epipe erange erofs espipe esrch estale etxtbsy exdev )a + @repo_timeout Pleroma.Config.get([Pleroma.Repo, :timeout], 15_000) + def compile_dir(dir) when is_binary(dir) do dir |> File.ls!() @@ -63,4 +65,21 @@ def posix_error_message(code) when code in @posix_error_codes do end def posix_error_message(_), do: "" + + @doc """ + Returns [timeout: integer] suitable for passing as an option to Repo functions. + + This function detects if the execution was triggered from IEx shell, Mix task, or + ./bin/pleroma_ctl and sets the timeout to :infinity, else returns the default timeout value. + """ + @spec query_timeout() :: [timeout: integer] + def query_timeout do + {parent, _, _, _} = Process.info(self(), :current_stacktrace) |> elem(1) |> Enum.fetch!(2) + + cond do + parent |> to_string |> String.starts_with?("Elixir.Mix.Task") -> [timeout: :infinity] + parent == :erl_eval -> [timeout: :infinity] + true -> [timeout: @repo_timeout] + end + end end diff --git a/lib/pleroma/web.ex b/lib/pleroma/web.ex index 8630f244b..d26931af9 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 @@ -131,7 +132,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 +231,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 efbf92c70..18368943d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -88,7 +88,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] @impl true def persist(%{"type" => type} = object, meta) when type in @object_types do with {:ok, object} <- Object.create(object) do @@ -630,7 +630,7 @@ defp fetch_activities_for_user(user, reading_user, params) do |> Map.put(:type, ["Create", "Announce"]) |> Map.put(:user, reading_user) |> Map.put(:actor_id, user.ap_id) - |> Map.put(:pinned_activity_ids, user.pinned_activities) + |> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects)) params = if User.blocks?(reading_user, user) do @@ -1075,8 +1075,18 @@ defp restrict_unlisted(query, %{restrict_unlisted: true}) do defp restrict_unlisted(query, _), do: query - defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do - from(activity in query, where: activity.id in ^ids) + defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do + from( + [activity, object: o] in query, + where: + fragment( + "(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)", + activity.data, + activity.data, + activity.data, + ^ids + ) + ) end defp restrict_pinned(query, _), do: query @@ -1419,6 +1429,9 @@ defp object_to_user_data(data) do invisible = data["invisible"] || false actor_type = data["type"] || "Person" + featured_address = data["featured"] + {:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address) + public_key = if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do data["publicKey"]["publicKeyPem"] @@ -1447,13 +1460,15 @@ defp object_to_user_data(data) do name: data["name"], follower_address: data["followers"], following_address: data["following"], + featured_address: featured_address, bio: data["summary"] || "", actor_type: actor_type, also_known_as: Map.get(data, "alsoKnownAs", []), public_key: public_key, inbox: data["inbox"], shared_inbox: shared_inbox, - accepts_chat_messages: accepts_chat_messages + accepts_chat_messages: accepts_chat_messages, + pinned_objects: pinned_objects } # nickname can be nil because of virtual actors @@ -1591,6 +1606,41 @@ def maybe_handle_clashing_nickname(data) do end end + def pin_data_from_featured_collection(%{ + "type" => type, + "orderedItems" => objects + }) + when type in ["OrderedCollection", "Collection"] do + Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end) + end + + def fetch_and_prepare_featured_from_ap_id(nil) do + {:ok, %{}} + end + + def fetch_and_prepare_featured_from_ap_id(ap_id) do + with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do + {:ok, pin_data_from_featured_collection(data)} + else + e -> + Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}") + {:ok, %{}} + end + end + + def pinned_fetch_task(nil), do: nil + + def pinned_fetch_task(%{pinned_objects: pins}) do + if Enum.all?(pins, fn {ap_id, _} -> + Object.get_cached_by_ap_id(ap_id) || + match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id)) + end) do + :ok + else + :error + end + end + def make_user_from_ap_id(ap_id) do user = User.get_cached_by_ap_id(ap_id) @@ -1598,6 +1648,8 @@ def make_user_from_ap_id(ap_id) do Transmogrifier.upgrade_user_from_ap_id(ap_id) else with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do + {:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end) + if user do user |> User.remote_user_changeset(data) 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 9d3dcc7f9..5aa3b281a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -543,4 +543,12 @@ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = |> json(object.data) end end + + def pinned(conn, %{"nickname" => nickname}) do + with %User{} = user <- User.get_cached_by_nickname(nickname) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(UserView.render("featured.json", %{user: user})) + end + end end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index f56bfc600..cde477710 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -223,7 +223,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()] @@ -273,4 +273,36 @@ defp object_action(actor, object) do "context" => object.data["context"] }, []} end + + @spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()} + def pin(%User{} = user, object) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "target" => pinned_url(user.nickname), + "object" => object.data["id"], + "actor" => user.ap_id, + "type" => "Add", + "to" => [Pleroma.Constants.as_public()], + "cc" => [user.follower_address] + }, []} + end + + @spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()} + def unpin(%User{} = user, object) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "target" => pinned_url(user.nickname), + "object" => object.data["id"], + "actor" => user.ap_id, + "type" => "Remove", + "to" => [Pleroma.Constants.as_public()], + "cc" => [user.follower_address] + }, []} + end + + defp pinned_url(nickname) when is_binary(nickname) do + Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname) + end end 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..f4c5db05c 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do @moduledoc "Filter local activities which have no content" @behaviour Pleroma.Web.ActivityPub.MRF - 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/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 62024c58c..d40348cb1 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -177,6 +177,14 @@ 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} + @impl true def filter(%{"type" => "Delete", "actor" => actor} = object) do %{host: actor_host} = URI.parse(actor) @@ -202,7 +210,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,6 +236,19 @@ 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 diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index f75744203..248a12a36 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator @@ -101,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] do with {:ok, object_data} <- cast_and_apply(object), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), {:ok, create_activity} <- @@ -113,9 +114,35 @@ def validate( end end + def validate(%{"type" => type} = object, meta) + when type in ~w[Event Question Audio Video Article Note] do + validator = + case type do + "Event" -> EventValidator + "Question" -> QuestionValidator + "Audio" -> AudioVideoValidator + "Video" -> AudioVideoValidator + "Article" -> ArticleNoteValidator + "Note" -> ArticleNoteValidator + end + + with {:ok, object} <- + object + |> validator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + + # Insert copy of hashtags as strings for the non-hashtag table indexing + tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) + object = Map.put(object, "tag", tag) + + {:ok, object, meta} + end + end + def validate(%{"type" => type} = object, meta) when type in ~w[Accept Reject Follow Update Like EmojiReact Announce - Event ChatMessage Question Audio Video Article Answer] do + ChatMessage Answer] do validator = case type do "Accept" -> AcceptRejectValidator @@ -125,12 +152,7 @@ def validate(%{"type" => type} = object, meta) "Like" -> LikeValidator "EmojiReact" -> EmojiReactValidator "Announce" -> AnnounceValidator - "Event" -> EventValidator "ChatMessage" -> ChatMessageValidator - "Question" -> QuestionValidator - "Audio" -> AudioVideoValidator - "Video" -> AudioVideoValidator - "Article" -> ArticleNoteValidator "Answer" -> AnswerValidator end @@ -143,6 +165,16 @@ def validate(%{"type" => type} = object, meta) end end + def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do + with {:ok, object} <- + object + |> AddRemoveValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def cast_and_apply(%{"type" => "ChatMessage"} = object) do ChatMessageValidator.cast_and_apply(object) end @@ -163,13 +195,13 @@ def cast_and_apply(%{"type" => "Event"} = object) do EventValidator.cast_and_apply(object) end - def cast_and_apply(%{"type" => "Article"} = object) do + def cast_and_apply(%{"type" => type} = object) when type in ~w[Article Note] do ArticleNoteValidator.cast_and_apply(object) end def cast_and_apply(o), do: {:error, {:validator_not_set, o}} - # is_struct/1 isn't present in Elixir 1.8.x + # is_struct/1 appears in Elixir 1.11 def stringify_keys(%{__struct__: _} = object) do object |> Map.from_struct() 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 d31e780c3..b577a1044 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 @@ -27,7 +27,7 @@ def cast_data(data) do |> cast(data, __schema__(:fields)) end - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Accept", "Reject"]) 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 new file mode 100644 index 000000000..f885aabe4 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do + use Ecto.Schema + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + 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: []) + end + + def cast_and_validate(data) do + {:ok, actor} = User.get_or_fetch_by_ap_id(data["actor"]) + + {:ok, actor} = maybe_refetch_user(actor) + + data + |> maybe_fix_data_for_mastodon(actor) + |> cast_data() + |> validate_data(actor) + end + + defp maybe_fix_data_for_mastodon(data, actor) do + # Mastodon sends pin/unpin objects without id, to, cc fields + data + |> Map.put_new("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id()) + |> Map.put_new("to", [Pleroma.Constants.as_public()]) + |> Map.put_new("cc", [actor.follower_address]) + end + + defp cast_data(data) do + cast(%__MODULE__{}, data, __schema__(:fields)) + end + + defp validate_data(changeset, actor) do + changeset + |> validate_required([:id, :target, :object, :actor, :type, :to, :cc]) + |> validate_inclusion(:type, ~w(Add Remove)) + |> validate_actor_presence() + |> validate_collection_belongs_to_actor(actor) + |> validate_object_presence() + end + + defp validate_collection_belongs_to_actor(changeset, actor) do + validate_change(changeset, :target, fn :target, target -> + if target == actor.featured_address do + [] + else + [target: "collection doesn't belong to actor"] + end + end) + end + + defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary(address) do + {:ok, user} + end + + defp maybe_refetch_user(%User{ap_id: ap_id}) do + Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id) + end +end 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 b08a33e68..a2f752ac3 100644 --- a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex @@ -50,7 +50,7 @@ def fix_after_cast(cng) do cng end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Announce"]) |> validate_required([:id, :type, :object, :actor, :to, :cc]) @@ -68,7 +68,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 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 15e4413cd..3451e1ff8 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 @@ -23,6 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do field(:name, :string) field(:inReplyTo, ObjectValidators.ObjectID) field(:attributedTo, ObjectValidators.ObjectID) + field(:context, :string) # TODO: Remove actor on objects field(:actor, ObjectValidators.ObjectID) @@ -46,11 +48,16 @@ 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 - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Answer"]) |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor]) 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 index b0388ef3b..193f85f49 100644 --- a/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do 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 @@ -22,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do field(:cc, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) + embeds_many(:tag, TagValidator) field(:type, :string) field(:name, :string) @@ -50,6 +50,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do field(:likes, {:array, ObjectValidators.ObjectID}, default: []) field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) + + field(:replies, {:array, ObjectValidators.ObjectID}, default: []) end def cast_and_apply(data) do @@ -65,36 +67,51 @@ def cast_and_validate(data) do 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(%{"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_defaults() - |> CommonFixes.fix_attribution() |> 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]) + |> cast(data, __schema__(:fields) -- [:attachment, :tag]) |> cast_embed(:attachment) + |> cast_embed(:tag) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Article", "Note"]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) 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 3175427ad..837787b9f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do use Ecto.Schema alias Pleroma.EctoType.ActivityPub.ObjectValidators - alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator import Ecto.Changeset @@ -21,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 @@ -52,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 @@ -60,7 +61,7 @@ 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") @@ -90,7 +91,7 @@ defp fix_url(data) do end end - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_inclusion(:type, ~w[Document Audio Image Video]) |> validate_required([:mediaType, :url, :type]) 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 4a96fef52..572687deb 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,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do use Ecto.Schema - alias Pleroma.EarmarkRenderer 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 @@ -23,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do field(:cc, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) + embeds_many(:tag, TagValidator) field(:type, :string) field(:name, :string) @@ -110,7 +109,7 @@ defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data) when is_binary(content) do content = content - |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) + |> Pleroma.Formatter.markdown_to_html() |> Pleroma.HTML.filter_tags() Map.put(data, "content", content) @@ -120,9 +119,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() @@ -132,11 +130,12 @@ def changeset(struct, data) do data = fix(data) struct - |> cast(data, __schema__(:fields) -- [:attachment]) + |> cast(data, __schema__(:fields) -- [:attachment, :tag]) |> cast_embed(:attachment) + |> cast_embed(:tag) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Audio", "Video"]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) 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 c5f77bb76..88948135f 100644 --- a/lib/pleroma/web/activity_pub/object_validators/block_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/block_validator.ex @@ -26,7 +26,7 @@ def cast_data(data) do |> cast(data, __schema__(:fields)) end - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Block"]) diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index 1189778f2..b153156b0 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -67,7 +67,7 @@ def changeset(struct, data) do |> cast_embed(:attachment) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["ChatMessage"]) |> validate_required([:id, :actor, :to, :type, :published]) 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..c958fcc5d 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -3,26 +3,55 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do + alias Pleroma.EctoType.ActivityPub.ObjectValidators 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, _meta) 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) 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 093549a45..be5074348 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do alias Pleroma.Object alias Pleroma.User + @spec validate_any_presence(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t() def validate_any_presence(cng, fields) do non_empty = fields |> Enum.map(fn field -> get_field(cng, field) end) |> Enum.any?(fn + nil -> false [] -> false _ -> true end) @@ -29,6 +31,7 @@ def validate_any_presence(cng, fields) do end end + @spec validate_actor_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t() def validate_actor_presence(cng, options \\ []) do field_name = Keyword.get(options, :field_name, :actor) @@ -47,6 +50,7 @@ def validate_actor_presence(cng, options \\ []) do end) end + @spec validate_object_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t() def validate_object_presence(cng, options \\ []) do field_name = Keyword.get(options, :field_name, :object) allowed_types = Keyword.get(options, :allowed_types, false) @@ -68,6 +72,7 @@ def validate_object_presence(cng, options \\ []) do end) end + @spec validate_object_or_user_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t() def validate_object_or_user_presence(cng, options \\ []) do field_name = Keyword.get(options, :field_name, :object) options = Keyword.put(options, :field_name, field_name) @@ -83,6 +88,7 @@ def validate_object_or_user_presence(cng, options \\ []) do if actor_cng.valid?, do: actor_cng, else: object_cng end + @spec validate_host_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t() def validate_host_match(cng, fields \\ [:id, :actor]) do if same_domain?(cng, fields) do cng @@ -95,6 +101,7 @@ def validate_host_match(cng, fields \\ [:id, :actor]) do end end + @spec validate_fields_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t() def validate_fields_match(cng, fields) do if map_unique?(cng, fields) do cng @@ -122,12 +129,14 @@ defp map_unique?(cng, fields, func \\ & &1) do end) end + @spec same_domain?(Ecto.Changeset.t(), [atom()]) :: boolean() def same_domain?(cng, fields \\ [:actor, :object]) do map_unique?(cng, fields, fn value -> URI.parse(value).host end) end # This figures out if a user is able to create, delete or modify something # based on the domain and superuser status + @spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t() def validate_modification_rights(cng) do actor = User.get_cached_by_ap_id(get_field(cng, :actor)) 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 8384c16a7..7a31a99bf 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 @@ -39,7 +39,7 @@ def cast_and_validate(data, meta \\ []) do |> validate_data(meta) end - def validate_data(cng, meta \\ []) do + defp validate_data(cng, meta) do cng |> validate_required([:id, :actor, :to, :type, :object]) |> validate_inclusion(:type, ["Create"]) 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 bf56a918c..d2de53049 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,8 +10,10 @@ 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 @@ -23,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do field(:type, :string) field(:to, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: []) + field(:bto, ObjectValidators.Recipients, default: []) + field(:bcc, ObjectValidators.Recipients, default: []) field(:object, ObjectValidators.ObjectID) field(:expires_at, ObjectValidators.DateTime) @@ -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"]) - 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() + |> 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 validate_data(cng, meta \\ []) do + def fix(data, meta) do + object = meta[:object_data] + + data + |> 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 fc1a79a72..7da67bf16 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -53,7 +53,7 @@ def add_deleted_activity_id(cng) do Tombstone Video } - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Delete"]) 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 1906e597e..ec7566515 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 @@ -70,7 +70,7 @@ def validate_emoji(cng) do end end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["EmojiReact"]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content]) 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 2e26726f8..fee2e997a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/event_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/event_validator.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do 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 @@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do field(:cc, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) + embeds_many(:tag, TagValidator) field(:type, :string) field(:name, :string) @@ -72,8 +72,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 @@ -81,11 +81,12 @@ def changeset(struct, data) do data = fix(data) struct - |> cast(data, __schema__(:fields) -- [:attachment]) + |> cast(data, __schema__(:fields) -- [:attachment, :tag]) |> cast_embed(:attachment) + |> cast_embed(:tag) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Event"]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) 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 6e428bacc..239cee5e7 100644 --- a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -27,7 +27,7 @@ def cast_data(data) do |> cast(data, __schema__(:fields)) end - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Follow"]) 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 30c40b238..509da507b 100644 --- a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex @@ -76,7 +76,7 @@ def fix_recipients(cng) do end end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Like"]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc]) 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 6b746c997..083d08ec4 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do 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 @@ -24,8 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do field(:cc, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: []) - # TODO: Write type - field(:tag, {:array, :map}, default: []) + embeds_many(:tag, TagValidator) field(:type, :string) field(:content, :string) field(:context, :string) @@ -83,8 +83,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 @@ -93,13 +93,14 @@ def changeset(struct, data) do data = fix(data) struct - |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment]) + |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment, :tag]) |> cast_embed(:attachment) |> cast_embed(:anyOf) |> cast_embed(:oneOf) + |> cast_embed(:tag) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Question"]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex new file mode 100644 index 000000000..751021585 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + + @primary_key false + embedded_schema do + # Common + field(:type, :string) + field(:name, :string) + + # Mention, Hashtag + field(:href, ObjectValidators.Uri) + + # Emoji + embeds_one :icon, IconObjectValidator, primary_key: false do + field(:type, :string) + field(:url, ObjectValidators.Uri) + end + + field(:updated, ObjectValidators.DateTime) + field(:id, ObjectValidators.Uri) + end + + def cast_and_validate(data) do + data + |> cast_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def changeset(struct, %{"type" => "Mention"} = data) do + struct + |> cast(data, [:type, :name, :href]) + |> validate_required([:type, :href]) + end + + def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do + name = + cond do + "#" <> name -> name + name -> name + end + |> String.downcase() + + data = Map.put(data, "name", name) + + struct + |> cast(data, [:type, :name, :href]) + |> validate_required([:type, :name]) + end + + def changeset(struct, %{"type" => "Emoji"} = data) do + data = Map.put(data, "name", String.trim(data["name"], ":")) + + struct + |> cast(data, [:type, :name, :updated, :id]) + |> cast_embed(:icon, with: &icon_changeset/2) + |> validate_required([:type, :name, :icon]) + end + + def icon_changeset(struct, data) do + struct + |> cast(data, [:type, :url]) + |> validate_inclusion(:type, ~w[Image]) + |> validate_required([:type, :url]) + end +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 783a79ddb..e8af60ffa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/undo_validator.ex @@ -38,7 +38,7 @@ def changeset(struct, data) do |> cast(data, __schema__(:fields)) end - def validate_data(data_cng) do + defp validate_data(data_cng) do data_cng |> validate_inclusion(:type, ["Undo"]) |> validate_required([:id, :type, :object, :actor, :to, :cc]) 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 a66d41400..6bb1dc7fa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex @@ -28,7 +28,7 @@ def cast_data(data) do |> cast(data, __schema__(:fields)) end - def validate_data(cng) do + defp validate_data(cng) do cng |> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_inclusion(:type, ["Update"]) diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 195596f94..0d6e8aad2 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do alias Pleroma.Config alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.Utils alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.ObjectValidator @@ -14,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) 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} -> @@ -40,19 +41,17 @@ def common_pipeline(object, meta) do end end - def do_common_pipeline(object, meta) do - with {_, {:ok, validated_object, meta}} <- - {:validate_object, @object_validator.validate(object, meta)}, - {_, {:ok, mrfd_object, meta}} <- - {:mrf_object, @mrf.pipeline_filter(validated_object, meta)}, - {_, {:ok, activity, meta}} <- - {:persist_object, @activity_pub.persist(mrfd_object, meta)}, - {_, {:ok, activity, meta}} <- - {:execute_side_effects, @side_effects.handle(activity, meta)}, - {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do - {:ok, activity, meta} + 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)}, + {_, {:ok, _}} <- {:federation, maybe_federate(message, meta)} do + {:ok, message, meta} else - {:mrf_object, {:reject, message, _}} -> {:reject, message} + {:mrf, {:reject, message, _}} -> {:reject, message} e -> {:error, e} end end @@ -61,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 = @@ -71,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..590beef64 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -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 0b9a9f0c5..674356d9a 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -203,6 +203,19 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) 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) @@ -276,10 +289,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, result = case deleted_object do %Object{} -> - with {:ok, deleted_object, activity} <- Object.delete(deleted_object), + with {:ok, deleted_object, _activity} <- Object.delete(deleted_object), {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]}, %User{} = user <- User.get_cached_by_ap_id(actor) do - User.remove_pinnned_activity(user, activity) + User.remove_pinned_object_id(user, deleted_object.data["id"]) {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) @@ -312,6 +325,63 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, end end + # Tasks this handles: + # - adds pin to user + # - removes expiration job for pinned activity, if was set for expiration + @impl true + def handle(%{data: %{"type" => "Add"} = data} = object, meta) do + with %User{} = user <- User.get_cached_by_ap_id(data["actor"]), + {:ok, _user} <- User.add_pinned_object_id(user, data["object"]) do + # if pinned activity was scheduled for deletion, we remove job + if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(meta[:activity_id]) do + Oban.cancel_job(expiration.id) + end + + {:ok, object, meta} + else + nil -> + {:error, :user_not_found} + + {:error, changeset} -> + if changeset.errors[:pinned_objects] do + {:error, :pinned_statuses_limit_reached} + else + changeset.errors + end + end + end + + # Tasks this handles: + # - removes pin from user + # - removes corresponding Add activity + # - if activity had expiration, recreates activity expiration job + @impl true + def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do + with %User{} = user <- User.get_cached_by_ap_id(data["actor"]), + {:ok, _user} <- User.remove_pinned_object_id(user, data["object"]) do + data["object"] + |> Activity.add_by_params_query(user.ap_id, user.featured_address) + |> Repo.delete_all() + + # if pinned activity was scheduled for deletion, we reschedule it for deletion + if meta[:expires_at] do + # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation + {:ok, expires_at} = + Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at]) + + Pleroma.Workers.PurgeExpiredActivity.enqueue(%{ + activity_id: meta[:activity_id], + expires_at: expires_at + }) + end + + {:ok, object, meta} + else + nil -> {:error, :user_not_found} + error -> error + end + end + # Nothing to do @impl true def handle(object, meta) do @@ -366,7 +436,7 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do end def handle_object_creation(%{"type" => objtype} = object, meta) - when objtype in ~w[Audio Video Question Event Article] do + when objtype in ~w[Audio Video Question Event Article Note] do with {:ok, object, meta} <- Pipeline.common_pipeline(object, 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 8c7d6a747..51c0cc860 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 @@ -423,10 +411,9 @@ def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id # - tags # - emoji def handle_incoming( - %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, + %{"type" => "Create", "object" => %{"type" => "Page"} = object} = data, options - ) - when objtype in ~w{Note Page} do + ) do actor = Containment.get_actor(data) with nil <- Activity.get_create_by_object_ap_id(object["id"]), @@ -518,14 +505,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} 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} @@ -534,7 +530,7 @@ def handle_incoming( end def handle_incoming(%{"type" => type} = data, _options) - when type in ~w{Like EmojiReact Announce} do + when type in ~w{Like EmojiReact Announce Add Remove} do with :ok <- ObjectValidator.fetch_actor_and_object(data), {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do @@ -564,7 +560,7 @@ def handle_incoming( Pipeline.common_pipeline(data, local: false) do {:ok, activity} else - {:error, {:validate_object, _}} = e -> + {:error, {:validate, _}} = e -> # Check if we have a create activity for this with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]), %Activity{data: %{"actor" => actor}} <- @@ -949,7 +945,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 +953,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) @@ -1000,6 +999,7 @@ def upgrade_user_from_ap_id(ap_id) do with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), {:ok, user} <- update_user(user, data) do + {:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end) TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) {:ok, user} else diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index a4dc469dc..1df53f79a 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 diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 8adc9878a..344da19d3 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do use Pleroma.Web, :view alias Pleroma.Keys + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Endpoint @@ -97,6 +99,7 @@ def render("user.json", %{user: user}) do "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", "outbox" => "#{user.ap_id}/outbox", + "featured" => "#{user.ap_id}/collections/featured", "preferredUsername" => user.nickname, "name" => user.name, "summary" => user.bio, @@ -245,6 +248,25 @@ def render("activity_collection_page.json", %{ |> Map.merge(pagination) end + def render("featured.json", %{ + user: %{featured_address: featured_address, pinned_objects: pinned_objects} + }) do + objects = + pinned_objects + |> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2) + |> Enum.map(fn {id, _} -> + ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)}) + end) + + %{ + "id" => featured_address, + "type" => "OrderedCollection", + "orderedItems" => objects, + "totalItems" => length(objects) + } + |> Map.merge(Utils.make_json_ld_header()) + end + defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, true, total) do diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 00234c0b0..2be59144d 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 @@ -127,7 +127,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/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/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/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index 85aa14869..1e245b291 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -105,6 +105,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) } } diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 4bdb8e281..802fbef3e 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -182,7 +182,34 @@ def pin_operation do parameters: [id_param()], responses: %{ 200 => status_response(), - 400 => Operation.response("Error", "application/json", ApiError) + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + allOf: [ApiError], + title: "Unprocessable Entity", + example: %{ + "error" => "You have already pinned the maximum number of statuses" + } + }), + 404 => + Operation.response("Not found", "application/json", %Schema{ + allOf: [ApiError], + title: "Unprocessable Entity", + example: %{ + "error" => "Record not found" + } + }), + 422 => + Operation.response( + "Unprocessable Entity", + "application/json", + %Schema{ + allOf: [ApiError], + title: "Unprocessable Entity", + example: %{ + "error" => "Someone else's status cannot be pinned" + } + } + ) } } end @@ -197,7 +224,22 @@ def unpin_operation do parameters: [id_param()], responses: %{ 200 => status_response(), - 400 => Operation.response("Error", "application/json", ApiError) + 400 => + Operation.response("Bad Request", "application/json", %Schema{ + allOf: [ApiError], + title: "Unprocessable Entity", + example: %{ + "error" => "You have already pinned the maximum number of statuses" + } + }), + 404 => + Operation.response("Not found", "application/json", %Schema{ + allOf: [ApiError], + title: "Unprocessable Entity", + example: %{ + "error" => "Record not found" + } + }) } } end 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..0cafbc719 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex @@ -0,0 +1,219 @@ +# 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 + + 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", + parameters: [ + Operation.parameter(:password, :query, :string, "Current password", required: true), + Operation.parameter(:new_password, :query, :string, "New password", required: true), + Operation.parameter( + :new_password_confirmation, + :query, + :string, + "New password, confirmation", + 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 + + def change_email_operation do + %Operation{ + tags: ["Account credentials"], + summary: "Change account email", + security: [%{"oAuth" => ["write:accounts"]}], + operationId: "UtilController.change_email", + parameters: [ + Operation.parameter(:password, :query, :string, "Current password", required: true), + Operation.parameter(:email, :query, :string, "New email", required: true) + ], + 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), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + 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") + ], + 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 +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/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 42fa98718..3d042dc19 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -194,6 +194,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do parent_visible: %Schema{ type: :boolean, description: "`true` if the parent post is visible to the user" + }, + pinned_at: %Schema{ + type: :string, + format: "date-time", + nullable: true, + description: + "A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned" } } }, 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 b003e30c7..1b5f8491e 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -228,17 +228,7 @@ def favorite_helper(user, id) do {:find_object, _} -> {:error, :not_found} - {:common_pipeline, - { - :error, - { - :validate_object, - { - :error, - changeset - } - } - }} = e -> + {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e -> if {:object, {"already liked by this actor", []}} in changeset.errors do {:ok, :already_liked} else @@ -411,29 +401,58 @@ def post(user, %{status: _} = data) do end end - def pin(id, %{ap_id: user_ap_id} = user) do - with %Activity{ - actor: ^user_ap_id, - data: %{"type" => "Create"}, - object: %Object{data: %{"type" => object_type}} - } = activity <- Activity.get_by_id_with_object(id), - true <- object_type in ["Note", "Article", "Question"], - true <- Visibility.is_public?(activity), - {:ok, _user} <- User.add_pinnned_activity(user, activity) do + @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} + def pin(id, %User{} = user) do + with %Activity{} = activity <- create_activity_by_id(id), + true <- activity_belongs_to_actor(activity, user.ap_id), + true <- object_type_is_allowed_for_pin(activity.object), + true <- activity_is_public(activity), + {:ok, pin_data, _} <- Builder.pin(user, activity.object), + {:ok, _pin, _} <- + Pipeline.common_pipeline(pin_data, + local: true, + activity_id: id + ) do {:ok, activity} else - {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} - _ -> {:error, dgettext("errors", "Could not pin")} + {:error, {:side_effects, error}} -> error + error -> error end end + defp create_activity_by_id(id) do + with nil <- Activity.create_by_id_with_object(id) do + {:error, :not_found} + end + end + + defp activity_belongs_to_actor(%{actor: actor}, actor), do: true + defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error} + + defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do + with false <- type in ["Note", "Article", "Question"] do + {:error, :not_allowed} + end + end + + defp activity_is_public(activity) do + with false <- Visibility.is_public?(activity) do + {:error, :visibility_error} + end + end + + @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()} def unpin(id, user) do - with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), - {:ok, _user} <- User.remove_pinnned_activity(user, activity) do + with %Activity{} = activity <- create_activity_by_id(id), + {:ok, unpin_data, _} <- Builder.unpin(user, activity.object), + {:ok, _unpin, _} <- + Pipeline.common_pipeline(unpin_data, + local: true, + activity_id: activity.id, + expires_at: activity.data["expires_at"], + featured_address: user.featured_address + ) do {:ok, activity} - else - {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} - _ -> {:error, dgettext("errors", "Could not unpin")} end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 4e6a3feb0..4cc34002d 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -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] @@ -286,7 +286,7 @@ def format_input(text, "text/html", options) do def format_input(text, "text/markdown", options) do text |> Formatter.mentions_escape(options) - |> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer}) + |> Formatter.markdown_to_html() |> Formatter.linkify(options) |> Formatter.html_escape("text/html") end 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..fa7879caf 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -28,7 +28,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/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex new file mode 100644 index 000000000..4920d65da --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex @@ -0,0 +1,108 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.AuthController do + use Pleroma.Web, :controller + + 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 = + Routes.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"] + + TwitterAPI.password_reset(nickname_or_email) + + json_response(conn, :no_content, "") + end + + defp local_mastodon_post_login_path(conn) do + case get_session(conn, :return_to) do + nil -> + Routes.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/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex index d25f84837..84621500e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex @@ -30,6 +30,12 @@ def call(conn, {:error, error_message}) do |> json(%{error: error_message}) end + def call(conn, {:error, status, message}) do + conn + |> put_status(status) + |> json(%{error: message}) + end + def call(conn, _) do conn |> put_status(:internal_server_error) 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/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/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index af93e453d..64b177eb3 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 @@ -108,7 +108,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 b051fca74..724dc5c5d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -260,6 +260,18 @@ def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) + else + {:error, :pinned_statuses_limit_reached} -> + {:error, "You have already pinned the maximum number of statuses"} + + {:error, :ownership_error} -> + {:error, :unprocessable_entity, "Someone else's status cannot be pinned"} + + {:error, :visibility_error} -> + {:error, :unprocessable_entity, "Non-public status cannot be pinned"} + + error -> + error 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..845f546d4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -37,8 +37,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 diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index ac25aefdd..9e9de33f6 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -292,6 +292,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 +404,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..3528185d5 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: %{ @@ -68,9 +69,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, 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/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 3753588f2..da44e0a74 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 @@ -152,6 +153,8 @@ def render( |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) + {pinned?, pinned_at} = pin_data(object, user) + %{ id: to_string(activity.id), uri: object.data["id"], @@ -173,7 +176,7 @@ def render( favourited: present?(favorited), bookmarked: present?(bookmarked), muted: false, - pinned: pinned?(activity, user), + pinned: pinned?, sensitive: false, spoiler_text: "", visibility: get_visibility(activity), @@ -184,7 +187,8 @@ def render( language: nil, emojis: [], pleroma: %{ - local: activity.local + local: activity.local, + pinned_at: pinned_at } } end @@ -256,7 +260,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" @@ -264,7 +268,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" ) @@ -316,6 +320,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} fn for_user, user -> User.mutes?(for_user, user) end ) + {pinned?, pinned_at} = pin_data(object, user) + %{ id: to_string(activity.id), uri: object.data["id"], @@ -339,7 +345,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} favourited: present?(favorited), bookmarked: present?(bookmarked), muted: muted, - pinned: pinned?(activity, user), + pinned: pinned?, sensitive: sensitive, spoiler_text: summary, visibility: get_visibility(object), @@ -360,7 +366,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} direct_conversation_id: direct_conversation_id, thread_muted: thread_muted?, emoji_reactions: emoji_reactions, - parent_visible: visible_for_user?(reply_to, opts[:for]) + parent_visible: visible_for_user?(reply_to, opts[:for]), + pinned_at: pinned_at } } end @@ -411,6 +418,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 @@ -433,8 +441,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 @@ -490,7 +514,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: [] @@ -529,8 +553,13 @@ defp present?(nil), do: false defp present?(false), do: false defp present?(_), do: true - defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}), - do: id in pinned_activities + defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do + if pinned_at = pinned_objects[object_id] do + {true, Utils.to_masto_date(pinned_at)} + else + {false, nil} + end + end defp build_emoji_map(emoji, users, current_user) do %{ 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/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/utils.ex b/lib/pleroma/web/metadata/utils.ex index de7195435..bc31d66b9 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -3,6 +3,7 @@ # 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 @@ -13,7 +14,7 @@ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do # 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() 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 ae040c6eb..73d485497 100644 --- a/lib/pleroma/web/o_auth/o_auth_controller.ex +++ b/lib/pleroma/web/o_auth/o_auth_controller.ex @@ -427,7 +427,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,6 +600,12 @@ def login(%User{} = user, %App{} = app, requested_scopes) when is_list(requested end end +<<<<<<< HEAD +======= + # Special case: Local MastodonFE + defp redirect_uri(%Plug.Conn{} = conn, "."), do: Routes.auth_url(conn, :login) + +>>>>>>> 0c56f9de0d607b88fd107e0bd13ef286f0629346 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/o_auth/o_auth_view.ex b/lib/pleroma/web/o_auth/o_auth_view.ex index 281bbcc3c..1419c96a2 100644 --- a/lib/pleroma/web/o_auth/o_auth_view.ex +++ b/lib/pleroma/web/o_auth/o_auth_view.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.OAuth.OAuthView do def render("token.json", %{token: token} = opts) do response = %{ + id: token.id, token_type: "Bearer", access_token: token.token, refresh_token: token.refresh_token, diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 165afd3b4..6e01c5497 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -47,7 +47,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/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/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/http_security_plug.ex b/lib/pleroma/web/plugs/http_security_plug.ex index 0025b042a..d1e6cc9d3 100644 --- a/lib/pleroma/web/plugs/http_security_plug.ex +++ b/lib/pleroma/web/plugs/http_security_plug.ex @@ -48,7 +48,8 @@ def headers do {"x-content-type-options", "nosniff"}, {"referrer-policy", referrer_policy}, {"x-download-options", "noopen"}, - {"content-security-policy", csp_string()} + {"content-security-policy", csp_string()}, + {"permissions-policy", "interest-cohort=()"} ] headers = diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 936053ee2..9695667b6 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -134,6 +134,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) @@ -607,18 +611,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) @@ -632,7 +630,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) @@ -640,7 +638,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 @@ -691,6 +689,7 @@ defmodule Pleroma.Web.Router do # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`: get("/users/:nickname/followers", ActivityPubController, :followers) get("/users/:nickname/following", ActivityPubController, :following) + get("/users/:nickname/collections/featured", ActivityPubController, :pinned) end scope "/", Pleroma.Web.ActivityPub do @@ -732,11 +731,11 @@ defmodule Pleroma.Web.Router do get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) 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 @@ -789,4 +788,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 fe485d10d..50f0927a3 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 new file mode 100644 index 000000000..6f2b98957 --- /dev/null +++ b/lib/pleroma/web/templates/masto_fe/index.html.eex @@ -0,0 +1,35 @@ + + + + + + +<%= 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..2846ec7e7 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 %>