diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 47b66ae69..941514e18 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,9 +16,15 @@ workflow:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH == "stable"
+ - if: $CI_PIPELINE_SOURCE == "web"
+ - if: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
+# Default artifacts configuration
+.default_artifacts: &default_artifacts
+ expire_in: 30 days
+
cache: &global_cache_policy
key: $CI_JOB_IMAGE-$CI_COMMIT_SHORT_SHA
paths:
@@ -56,6 +62,7 @@ check-changelog:
before_script: ''
after_script: ''
cache: {}
+ artifacts: *default_artifacts
script:
- apk add git
- sh ./tools/check-changelog
@@ -71,6 +78,7 @@ check-changelog:
.using-ci-base:
tags:
- amd64
+ artifacts: *default_artifacts
build-1.15.8-otp-26:
extends:
@@ -101,8 +109,12 @@ spec-build:
artifacts:
paths:
- spec.json
+ reports:
+ dotenv: build.env
+ expire_in: 42 years
script:
- mix pleroma.openapi_spec spec.json
+ - echo "SPEC_BUILD_JOB_ID=$CI_JOB_ID" >> build.env
benchmark:
extends:
@@ -153,6 +165,7 @@ unit-testing-1.15.8-otp-26:
- su testuser -c "HOME=/home/testuser mix pleroma.test_runner --cover --preload-modules"
coverage: '/^Line total: ([^ ]*%)$/'
artifacts:
+ expire_in: 30 days
reports:
coverage_report:
coverage_format: cobertura
@@ -171,6 +184,7 @@ unit-testing-1.18.3-otp-27:
formatting-1.15:
extends: .build_changes_policy
+ artifacts: *default_artifacts
image: &formatting_elixir elixir:1.15-alpine
stage: lint
cache: *testing_cache_policy
@@ -185,6 +199,7 @@ formatting-1.15:
cycles-1.15:
extends: .build_changes_policy
+ artifacts: *default_artifacts
image: *formatting_elixir
stage: lint
cache: {}
@@ -208,7 +223,7 @@ dialyzer:
- .using-ci-base
stage: lint
allow_failure: true
- when: manual
+ when: manual
cache: *testing_cache_policy
tags:
- feld
@@ -217,15 +232,13 @@ dialyzer:
docs-deploy:
stage: deploy
- cache: *testing_cache_policy
- image: alpine:latest
+ trigger:
+ project: pleroma/docs
+ branch: master
+ strategy: depend
only:
- stable@pleroma/pleroma
- develop@pleroma/pleroma
- before_script:
- - apk add curl
- script:
- - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline
review_app:
image: alpine:3.9
stage: deploy
@@ -241,6 +254,7 @@ review_app:
except:
- master
- develop
+ artifacts: *default_artifacts
script:
- echo "$CI_ENVIRONMENT_SLUG"
- mkdir -p ~/.ssh
@@ -257,21 +271,19 @@ review_app:
spec-deploy:
stage: deploy
- artifacts:
- paths:
- - spec.json
+ trigger:
+ project: pleroma/api-docs
+ branch: master
+ strategy: depend
only:
- develop@pleroma/pleroma
- image: alpine:latest
- before_script:
- - apk add curl
- script:
- - curl --fail-with-body -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
-
+ variables:
+ SPEC_BUILD_JOB_ID: $SPEC_BUILD_JOB_ID
stop_review_app:
image: alpine:3.9
stage: deploy
+ artifacts: *default_artifacts
before_script:
- apk update && apk add openssh-client git
when: manual
diff --git a/changelog.d/authorized_fetch.fix b/changelog.d/authorized_fetch.fix
new file mode 100644
index 000000000..1db8e88c9
--- /dev/null
+++ b/changelog.d/authorized_fetch.fix
@@ -0,0 +1 @@
+Fix fetching public keys with authorized fetch enabled
\ No newline at end of file
diff --git a/changelog.d/blocked-muted-swagger.change b/changelog.d/blocked-muted-swagger.change
new file mode 100644
index 000000000..12bba8612
--- /dev/null
+++ b/changelog.d/blocked-muted-swagger.change
@@ -0,0 +1 @@
+Use separate schemas for muted/blocked accounts lists
\ No newline at end of file
diff --git a/changelog.d/changelog-checker.skip b/changelog.d/changelog-checker.skip
new file mode 100644
index 000000000..e910a649f
--- /dev/null
+++ b/changelog.d/changelog-checker.skip
@@ -0,0 +1 @@
+Fix CI changelog checker
diff --git a/changelog.d/ci-artifacts.skip b/changelog.d/ci-artifacts.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/description.skip b/changelog.d/description.skip
new file mode 100644
index 000000000..bbcfb2e13
--- /dev/null
+++ b/changelog.d/description.skip
@@ -0,0 +1 @@
+Use :list_behaviour_implementations for LanguageDetector and Translation providers
diff --git a/changelog.d/endorsements-api.change b/changelog.d/endorsements-api.change
new file mode 100644
index 000000000..279392c66
--- /dev/null
+++ b/changelog.d/endorsements-api.change
@@ -0,0 +1 @@
+Support new Mastodon API for endorsed accounts
diff --git a/changelog.d/fediindex.change b/changelog.d/fediindex.change
new file mode 100644
index 000000000..b9bef2762
--- /dev/null
+++ b/changelog.d/fediindex.change
@@ -0,0 +1 @@
+Allow FediIndex crawler bot by default
\ No newline at end of file
diff --git a/changelog.d/filter-user-capabilities.add b/changelog.d/filter-user-capabilities.add
new file mode 100644
index 000000000..fe2459210
--- /dev/null
+++ b/changelog.d/filter-user-capabilities.add
@@ -0,0 +1 @@
+Allow filtering users with `accepts_chat_messages` capability
\ No newline at end of file
diff --git a/changelog.d/instance-view-timeline-access.add b/changelog.d/instance-view-timeline-access.add
new file mode 100644
index 000000000..eb414e786
--- /dev/null
+++ b/changelog.d/instance-view-timeline-access.add
@@ -0,0 +1 @@
+Add `timelines_access` to InstanceView
diff --git a/changelog.d/local-nickname-regex.fix b/changelog.d/local-nickname-regex.fix
new file mode 100644
index 000000000..81ddd9cff
--- /dev/null
+++ b/changelog.d/local-nickname-regex.fix
@@ -0,0 +1 @@
+Use end-of-string in regex for local `get_by_nickname`
diff --git a/changelog.d/lookup-restrict-unauthenticated.fix b/changelog.d/lookup-restrict-unauthenticated.fix
new file mode 100644
index 000000000..a062b9361
--- /dev/null
+++ b/changelog.d/lookup-restrict-unauthenticated.fix
@@ -0,0 +1 @@
+Respect restrict_unauthenticated in /api/v1/accounts/lookup
diff --git a/changelog.d/mastodon-quotes-updates.change b/changelog.d/mastodon-quotes-updates.change
new file mode 100644
index 000000000..4c01ec106
--- /dev/null
+++ b/changelog.d/mastodon-quotes-updates.change
@@ -0,0 +1 @@
+Use Mastodon-compatible route for quotes list and param for quotes count
diff --git a/changelog.d/mrf-inlinequotes-mastodon.fix b/changelog.d/mrf-inlinequotes-mastodon.fix
new file mode 100644
index 000000000..638b3fde3
--- /dev/null
+++ b/changelog.d/mrf-inlinequotes-mastodon.fix
@@ -0,0 +1 @@
+MRF InlineQuotePolicy: Don't inline quoted post URL in Mastodon quote posts
diff --git a/changelog.d/nodeinfo-content-type.fix b/changelog.d/nodeinfo-content-type.fix
new file mode 100644
index 000000000..255fab475
--- /dev/null
+++ b/changelog.d/nodeinfo-content-type.fix
@@ -0,0 +1 @@
+Fix NodeInfo content-type
diff --git a/changelog.d/normalize-actor-image-hrefs.fix b/changelog.d/normalize-actor-image-hrefs.fix
new file mode 100644
index 000000000..33d222391
--- /dev/null
+++ b/changelog.d/normalize-actor-image-hrefs.fix
@@ -0,0 +1 @@
+Add Actor images normalization from array of urls to string
diff --git a/changelog.d/notification-cleanup.skip b/changelog.d/notification-cleanup.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/notification-view-deduplicate.skip b/changelog.d/notification-view-deduplicate.skip
new file mode 100644
index 000000000..769352692
--- /dev/null
+++ b/changelog.d/notification-view-deduplicate.skip
@@ -0,0 +1 @@
+remove duplicated code from notificationview
diff --git a/changelog.d/order-favourites-reblogs.change b/changelog.d/order-favourites-reblogs.change
new file mode 100644
index 000000000..67c235d62
--- /dev/null
+++ b/changelog.d/order-favourites-reblogs.change
@@ -0,0 +1 @@
+Order favourites and reblogs list from newest to oldest
diff --git a/changelog.d/outgoing-follow-requests.add b/changelog.d/outgoing-follow-requests.add
new file mode 100644
index 000000000..a898bcf6e
--- /dev/null
+++ b/changelog.d/outgoing-follow-requests.add
@@ -0,0 +1 @@
+Add /api/v1/pleroma/outgoing_follow_requests
diff --git a/changelog.d/pin-chats.fix b/changelog.d/pin-chats.fix
new file mode 100644
index 000000000..e7520ceaf
--- /dev/null
+++ b/changelog.d/pin-chats.fix
@@ -0,0 +1 @@
+Allow to pin/unpip chats
diff --git a/changelog.d/plaroma.skip b/changelog.d/plaroma.skip
new file mode 100644
index 000000000..184ca07e0
--- /dev/null
+++ b/changelog.d/plaroma.skip
@@ -0,0 +1 @@
+i don't think it's called plaroma
\ No newline at end of file
diff --git a/changelog.d/preferred-frontend.add b/changelog.d/preferred-frontend.add
new file mode 100644
index 000000000..145e9451b
--- /dev/null
+++ b/changelog.d/preferred-frontend.add
@@ -0,0 +1 @@
+Allow users to select preferred frontend
diff --git a/changelog.d/remote-url.fix b/changelog.d/remote-url.fix
new file mode 100644
index 000000000..9be84a878
--- /dev/null
+++ b/changelog.d/remote-url.fix
@@ -0,0 +1 @@
+`remote_url` links to unproxied URL
diff --git a/changelog.d/rich-media-user-agent.add b/changelog.d/rich-media-user-agent.add
new file mode 100644
index 000000000..5c1e2b134
--- /dev/null
+++ b/changelog.d/rich-media-user-agent.add
@@ -0,0 +1 @@
+Allow setting custom user-agent for fetching rich media content
diff --git a/changelog.d/rss-redirect.change b/changelog.d/rss-redirect.change
new file mode 100644
index 000000000..cd8b099aa
--- /dev/null
+++ b/changelog.d/rss-redirect.change
@@ -0,0 +1 @@
+Redirect /users/:nickname.rss to /users/:nickname/feed.rss instead of .atom
\ No newline at end of file
diff --git a/changelog.d/scrobbles-scope.change b/changelog.d/scrobbles-scope.change
new file mode 100644
index 000000000..3c31eadcc
--- /dev/null
+++ b/changelog.d/scrobbles-scope.change
@@ -0,0 +1 @@
+Add `write:scrobbles` and `read:scrobbles` scope for scrobbling
diff --git a/changelog.d/scrubber-inline-quotes-mastodon.add b/changelog.d/scrubber-inline-quotes-mastodon.add
new file mode 100644
index 000000000..a8006e423
--- /dev/null
+++ b/changelog.d/scrubber-inline-quotes-mastodon.add
@@ -0,0 +1 @@
+Scrubber: Allow `quote-inline` class in
tags used by Mastodon quotes
\ No newline at end of file
diff --git a/changelog.d/scrubber-span-classes.change b/changelog.d/scrubber-span-classes.change
new file mode 100644
index 000000000..4ba5dfa91
--- /dev/null
+++ b/changelog.d/scrubber-span-classes.change
@@ -0,0 +1 @@
+Allow "invisible" and "ellipsis" classes for span tags to match Mastodon behavior
diff --git a/changelog.d/status-push-notification.fix b/changelog.d/status-push-notification.fix
new file mode 100644
index 000000000..ed0bbff33
--- /dev/null
+++ b/changelog.d/status-push-notification.fix
@@ -0,0 +1 @@
+Send push notifications for statuses from subscribed accounts
diff --git a/changelog.d/stream-marker-updates.add b/changelog.d/stream-marker-updates.add
new file mode 100644
index 000000000..e9fda3e59
--- /dev/null
+++ b/changelog.d/stream-marker-updates.add
@@ -0,0 +1 @@
+Stream marker updates
diff --git a/changelog.d/translation-provider-mozhi.add b/changelog.d/translation-provider-mozhi.add
new file mode 100644
index 000000000..c3cf5940a
--- /dev/null
+++ b/changelog.d/translation-provider-mozhi.add
@@ -0,0 +1 @@
+Support Mozhi translation provider
diff --git a/changelog.d/translation-provider-translatelocally.add b/changelog.d/translation-provider-translatelocally.add
new file mode 100644
index 000000000..635e80061
--- /dev/null
+++ b/changelog.d/translation-provider-translatelocally.add
@@ -0,0 +1 @@
+Support translateLocally translation provider
diff --git a/changelog.d/url-encoding-pt2.fix b/changelog.d/url-encoding-pt2.fix
new file mode 100644
index 000000000..bc6857e02
--- /dev/null
+++ b/changelog.d/url-encoding-pt2.fix
@@ -0,0 +1 @@
+Fix sometimes incorrect URI percent encoding
diff --git a/config/description.exs b/config/description.exs
index c61a344e8..c388d17c3 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2131,6 +2131,11 @@ config :pleroma, :config_description, [
description:
"Amount of milliseconds after which the HTTP request is forcibly terminated.",
suggestions: [5_000]
+ },
+ %{
+ key: :user_agent,
+ type: :string,
+ description: "Custom User-Agent header to be used when fetching rich media content."
}
]
},
@@ -3328,6 +3333,12 @@ config :pleroma, :config_description, [
description:
"A map containing available frontends and parameters for their installation.",
children: frontend_options
+ },
+ %{
+ key: :pickable,
+ type: {:list, :string},
+ description:
+ "A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable."
}
]
},
@@ -3534,9 +3545,7 @@ config :pleroma, :config_description, [
%{
key: :provider,
type: :module,
- suggestions: [
- Pleroma.Language.LanguageDetector.Fasttext
- ]
+ suggestions: {:list_behaviour_implementations, Pleroma.Language.LanguageDetector.Provider}
},
%{
group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext},
@@ -3556,10 +3565,7 @@ config :pleroma, :config_description, [
%{
key: :provider,
type: :module,
- suggestions: [
- Pleroma.Language.Translation.Deepl,
- Pleroma.Language.Translation.Libretranslate
- ]
+ suggestions: {:list_behaviour_implementations, Pleroma.Language.Translation.Provider}
},
%{
group: {:subgroup, Pleroma.Language.Translation.Deepl},
@@ -3588,6 +3594,27 @@ config :pleroma, :config_description, [
label: "LibreTranslate API Key",
type: :string,
suggestions: ["YOUR_API_KEY"]
+ },
+ %{
+ group: {:subgroup, Pleroma.Language.Translation.TranslateLocally},
+ key: :intermediary_language,
+ label:
+ "translateLocally intermediary language (used when direct source->target model is not available)",
+ type: :string,
+ suggestions: ["en"]
+ },
+ %{
+ group: {:subgroup, Pleroma.Language.Translation.Mozhi},
+ key: :base_url,
+ label: "Mozhi instance URL",
+ type: :string
+ },
+ %{
+ group: {:subgroup, Pleroma.Language.Translation.Mozhi},
+ key: :engine,
+ label: "Engine used for Mozhi",
+ type: :string,
+ suggestions: ["libretranslate"]
}
]
}
diff --git a/docs/development/API/chats.md b/docs/development/API/chats.md
index f50144c86..ee2169adf 100644
--- a/docs/development/API/chats.md
+++ b/docs/development/API/chats.md
@@ -66,9 +66,9 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 2,
- "last_message" : {...}, // The last message in that chat
+ "id": "1",
+ "unread": 2,
+ "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
@@ -93,8 +93,8 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 0,
+ "id": "1",
+ "unread": 0,
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
@@ -111,7 +111,7 @@ The modified chat message
### Getting a list of Chats
-`GET /api/v1/pleroma/chats`
+`GET /api/v2/pleroma/chats`
This will return a list of chats that you have been involved in, sorted by their
last update (so new chats will be at the top).
@@ -119,6 +119,7 @@ last update (so new chats will be at the top).
Parameters:
- with_muted: Include chats from muted users (boolean).
+- pinned: Include only pinned chats (boolean).
Returned data:
@@ -130,16 +131,16 @@ Returned data:
"username": "somenick",
...
},
- "id" : "1",
- "unread" : 2,
- "last_message" : {...}, // The last message in that chat
+ "id": "1",
+ "unread": 2,
+ "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
]
```
The recipient of messages that are sent to this chat is given by their AP ID.
-No pagination is implemented for now.
+The usual pagination options are implemented.
### Getting the messages for a Chat
@@ -226,6 +227,32 @@ Deleting a chat message for given Chat id works like this:
Returned data is the deleted message.
+### Pinning a chat
+
+Pinning a chat works like this:
+
+`POST /api/v1/pleroma/chats/:id/pin`
+
+Returned data:
+
+```json
+{
+ "account": {
+ "id": "someflakeid",
+ "username": "somenick",
+ ...
+ },
+ "id": "1",
+ "unread": 0,
+ "updated_at": "2020-04-21T15:11:46.000Z",
+ "pinned": true,
+}
+```
+
+To unpin a pinned chat, use:
+
+`POST /api/v1/pleroma/chats/:id/unpin`
+
### Notifications
There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index fa7e3e5ab..052b2716b 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -39,7 +39,6 @@ Has these additional fields under the `pleroma` object:
- `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.
-- `quotes_count`: the count of status quotes.
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
- `list_id`: the ID of the list the post is addressed to (if any, only returned to author).
diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md
index b17f61cbb..7946ba1f6 100644
--- a/docs/development/API/pleroma_api.md
+++ b/docs/development/API/pleroma_api.md
@@ -684,6 +684,7 @@ Audio scrobbling in Pleroma is **deprecated**.
### Creates a new Listen activity for an account
* Method `POST`
* Authentication: required
+* OAuth scope: `write:scrobbles`
* Params:
* `title`: the title of the media playing
* `album`: the album of the media playing [optional]
diff --git a/docs/installation/optional/media_graphics_packages.md b/docs/installation/optional/media_graphics_packages.md
index ad01d47d1..89125d1c6 100644
--- a/docs/installation/optional/media_graphics_packages.md
+++ b/docs/installation/optional/media_graphics_packages.md
@@ -16,7 +16,7 @@ Note: the packages are not required with the current default settings of Pleroma
It is required for the following Pleroma features:
-* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
+* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Pleroma.Upload/filters` in `config/config.exs`)
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
## `ffmpeg`
@@ -33,5 +33,5 @@ It is required for the following Pleroma features:
It is required for the following Pleroma features:
-* `Pleroma.Upload.Filters.Exiftool.StripLocation` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
-* `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
+* `Pleroma.Upload.Filters.Exiftool.StripLocation` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)
+* `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index 5c4dbc1ff..926176bfa 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -25,6 +25,8 @@ defmodule Pleroma.Chat do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:recipient, :string)
+ field(:pinned, :boolean)
+
timestamps()
end
@@ -94,4 +96,16 @@ defmodule Pleroma.Chat do
order_by: [desc: c.updated_at]
)
end
+
+ def pin(%__MODULE__{} = chat) do
+ chat
+ |> cast(%{pinned: true}, [:pinned])
+ |> Repo.update()
+ end
+
+ def unpin(%__MODULE__{} = chat) do
+ chat
+ |> cast(%{pinned: false}, [:pinned])
+ |> Repo.update()
+ end
end
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 92ca11494..f02607273 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -132,6 +132,13 @@ defmodule Pleroma.Constants do
do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
)
+ # List of allowed chars in the path segment of a URI
+ # unreserved, sub-delims, ":", "@" and "/" allowed as the separator in path
+ # https://datatracker.ietf.org/doc/html/rfc3986
+ const(uri_path_allowed_reserved_chars,
+ do: ~c"!$&'()*+,;=/:@"
+ )
+
const(upload_object_types, do: ["Document", "Image"])
const(activity_json_canonical_mime_type,
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 495488dfd..653feb32f 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -157,6 +157,16 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.all()
end
+ def get_outgoing_follow_requests(%User{id: id}) do
+ __MODULE__
+ |> join(:inner, [r], f in assoc(r, :following))
+ |> where([r], r.state == ^:follow_pending)
+ |> where([r], r.follower_id == ^id)
+ |> where([r, f], f.is_active == true)
+ |> select([r, f], f)
+ |> Repo.all()
+ end
+
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex
index 9a0868d33..bdeb2171e 100644
--- a/lib/pleroma/http.ex
+++ b/lib/pleroma/http.ex
@@ -131,31 +131,4 @@ defmodule Pleroma.HTTP do
defp default_middleware,
do: [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.EncodeUrl]
-
- def encode_url(url) when is_binary(url) do
- URI.parse(url)
- |> then(fn parsed ->
- path = encode_path(parsed.path)
- query = encode_query(parsed.query)
-
- %{parsed | path: path, query: query}
- end)
- |> URI.to_string()
- end
-
- defp encode_path(nil), do: nil
-
- defp encode_path(path) when is_binary(path) do
- path
- |> URI.decode()
- |> URI.encode()
- end
-
- defp encode_query(nil), do: nil
-
- defp encode_query(query) when is_binary(query) do
- query
- |> URI.decode_query()
- |> URI.encode_query()
- end
end
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
index f3be1f3d0..f3451cf9c 100644
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -16,7 +16,12 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
config_opts = Pleroma.Config.get([:http, :adapter], [])
+ url_encoding =
+ Keyword.new()
+ |> Keyword.put(:path_encode_fun, fn path -> path end)
+
@defaults
+ |> Keyword.merge(url_encoding)
|> Keyword.merge(config_opts)
|> Keyword.merge(connection_opts)
|> add_scheme_opts(uri)
diff --git a/lib/pleroma/language/translation/mozhi.ex b/lib/pleroma/language/translation/mozhi.ex
new file mode 100644
index 000000000..958f2ef57
--- /dev/null
+++ b/lib/pleroma/language/translation/mozhi.ex
@@ -0,0 +1,109 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.Mozhi do
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+
+ alias Pleroma.Language.Translation.Provider
+
+ use Provider
+
+ @behaviour Provider
+
+ @name "Mozhi"
+
+ @impl Provider
+ def configured?, do: not_empty_string(base_url()) and not_empty_string(engine())
+
+ @impl Provider
+ def translate(content, source_language, target_language) do
+ endpoint =
+ base_url()
+ |> URI.merge("/api/translate")
+ |> URI.to_string()
+
+ case Pleroma.HTTP.get(
+ endpoint <>
+ "?" <>
+ URI.encode_query(%{
+ engine: engine(),
+ text: content,
+ from: source_language,
+ to: target_language
+ }),
+ [{"Accept", "application/json"}]
+ ) do
+ {:ok, %{status: 200} = res} ->
+ %{
+ "translated-text" => content,
+ "source_language" => source_language
+ } = Jason.decode!(res.body)
+
+ {:ok,
+ %{
+ content: content,
+ detected_source_language: source_language,
+ provider: @name
+ }}
+
+ _ ->
+ {:error, :internal_server_error}
+ end
+ end
+
+ @impl Provider
+ def supported_languages(type) when type in [:source, :target] do
+ path =
+ case type do
+ :source -> "/api/source_languages"
+ :target -> "/api/target_languages"
+ end
+
+ endpoint =
+ base_url()
+ |> URI.merge(path)
+ |> URI.to_string()
+
+ case Pleroma.HTTP.get(
+ endpoint <>
+ "?" <>
+ URI.encode_query(%{
+ engine: engine()
+ }),
+ [{"Accept", "application/json"}]
+ ) do
+ {:ok, %{status: 200} = res} ->
+ languages =
+ Jason.decode!(res.body)
+ |> Enum.map(fn %{"Id" => language} -> language end)
+
+ {:ok, languages}
+
+ _ ->
+ {:error, :internal_server_error}
+ end
+ end
+
+ @impl Provider
+ def languages_matrix do
+ with {:ok, source_languages} <- supported_languages(:source),
+ {:ok, target_languages} <- supported_languages(:target) do
+ {:ok,
+ Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)}
+ else
+ {:error, error} -> {:error, error}
+ end
+ end
+
+ @impl Provider
+ def name, do: @name
+
+ defp base_url do
+ Pleroma.Config.get([__MODULE__, :base_url])
+ end
+
+ defp engine do
+ Pleroma.Config.get([__MODULE__, :engine])
+ end
+end
diff --git a/lib/pleroma/language/translation/translate_locally.ex b/lib/pleroma/language/translation/translate_locally.ex
new file mode 100644
index 000000000..7eaa95e7b
--- /dev/null
+++ b/lib/pleroma/language/translation/translate_locally.ex
@@ -0,0 +1,129 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.TranslateLocally do
+ alias Pleroma.Language.Translation.Provider
+
+ use Provider
+
+ @behaviour Provider
+
+ @name "translateLocally"
+
+ @impl Provider
+ def missing_dependencies do
+ if Pleroma.Utils.command_available?("translateLocally") do
+ []
+ else
+ ["translateLocally"]
+ end
+ end
+
+ @impl Provider
+ def configured?, do: is_map(models())
+
+ @impl Provider
+ def translate(content, source_language, target_language) do
+ model =
+ models()
+ |> Map.get(source_language, %{})
+ |> Map.get(target_language)
+
+ models =
+ if model do
+ [model]
+ else
+ [
+ models()
+ |> Map.get(source_language, %{})
+ |> Map.get(intermediary_language()),
+ models()
+ |> Map.get(intermediary_language(), %{})
+ |> Map.get(target_language)
+ ]
+ end
+
+ translated_content =
+ Enum.reduce(models, content, fn model, content ->
+ text_path = Path.join(System.tmp_dir!(), "translateLocally-#{Ecto.UUID.generate()}")
+
+ File.write(text_path, content)
+
+ translated_content =
+ case System.cmd("translateLocally", ["-m", model, "-i", text_path, "--html"]) do
+ {content, _} -> content
+ _ -> nil
+ end
+
+ File.rm(text_path)
+
+ translated_content
+ end)
+
+ {:ok,
+ %{
+ content: translated_content,
+ detected_source_language: source_language,
+ provider: @name
+ }}
+ end
+
+ @impl Provider
+ def supported_languages(:source) do
+ languages =
+ languages_matrix()
+ |> elem(1)
+ |> Map.keys()
+
+ {:ok, languages}
+ end
+
+ @impl Provider
+ def supported_languages(:target) do
+ languages =
+ languages_matrix()
+ |> elem(1)
+ |> Map.values()
+ |> List.flatten()
+ |> Enum.uniq()
+
+ {:ok, languages}
+ end
+
+ @impl Provider
+ def languages_matrix do
+ languages =
+ models()
+ |> Map.to_list()
+ |> Enum.map(fn {key, value} -> {key, Map.keys(value)} end)
+ |> Enum.into(%{})
+
+ matrix =
+ if intermediary_language() do
+ languages
+ |> Map.to_list()
+ |> Enum.map(fn {key, value} ->
+ with_intermediary =
+ (((value ++ languages[intermediary_language()])
+ |> Enum.uniq()) --
+ [key])
+ |> Enum.sort()
+
+ {key, with_intermediary}
+ end)
+ |> Enum.into(%{})
+ else
+ languages
+ end
+
+ {:ok, matrix}
+ end
+
+ @impl Provider
+ def name, do: @name
+
+ defp models, do: Pleroma.Config.get([__MODULE__, :models])
+
+ defp intermediary_language, do: Pleroma.Config.get([__MODULE__, :intermediary_language])
+end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 80844ed71..b8693c3a8 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -282,10 +282,15 @@ defmodule Pleroma.Notification do
select: n.id
)
- Multi.new()
- |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
- |> Marker.multi_set_last_read_id(user, "notifications")
- |> Repo.transaction()
+ {:ok, %{marker: marker}} =
+ Multi.new()
+ |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
+
+ Streamer.stream(["user", "user:notification"], marker)
+
+ {:ok, %{marker: marker}}
end
@spec read_one(User.t(), String.t()) ::
@@ -526,9 +531,7 @@ defmodule Pleroma.Notification do
%Activity{data: %{"type" => "Create"}} = activity,
local_only
) do
- notification_enabled_ap_ids =
- []
- |> Utils.maybe_notify_subscribers(activity)
+ notification_enabled_ap_ids = Utils.get_notified_subscribers(activity)
potential_receivers =
User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index cd58f29e4..bb55a4984 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do
+ alias Pleroma.Utils.URIEncoding
+
@range_headers ~w(range if-range)
@keep_req_headers ~w(accept accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match) ++ @range_headers
@@ -155,11 +157,12 @@ defmodule Pleroma.ReverseProxy do
end
defp request(method, url, headers, opts) do
- Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
url = maybe_encode_url(url)
+ Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
+
case client().request(method, url, headers, "", opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
@@ -459,9 +462,9 @@ defmodule Pleroma.ReverseProxy do
# Also do it for test environment
defp maybe_encode_url(url) do
case Application.get_env(:tesla, :adapter) do
- Tesla.Adapter.Hackney -> Pleroma.HTTP.encode_url(url)
- {Tesla.Adapter.Finch, _} -> Pleroma.HTTP.encode_url(url)
- Tesla.Mock -> Pleroma.HTTP.encode_url(url)
+ Tesla.Adapter.Hackney -> URIEncoding.encode_url(url)
+ {Tesla.Adapter.Finch, _} -> URIEncoding.encode_url(url)
+ Tesla.Mock -> URIEncoding.encode_url(url)
_ -> url
end
end
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index d3e986912..0aa5f5715 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -7,6 +7,11 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def request(method, url, headers, body, opts \\ []) do
+ opts =
+ Keyword.put_new(opts, :path_encode_fun, fn path ->
+ path
+ end)
+
:hackney.request(method, url, headers, body, opts)
end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index 47d9d46f6..fca61799b 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -54,7 +54,7 @@ defmodule Pleroma.Signature do
def fetch_public_key(conn) do
with {:ok, actor_id} <- get_actor_id(conn),
- {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
+ {:ok, public_key} <- User.get_or_fetch_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
diff --git a/lib/pleroma/tesla/middleware/encode_url.ex b/lib/pleroma/tesla/middleware/encode_url.ex
index 32c559d3b..677577c2f 100644
--- a/lib/pleroma/tesla/middleware/encode_url.ex
+++ b/lib/pleroma/tesla/middleware/encode_url.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Tesla.Middleware.EncodeUrl do
@impl Tesla.Middleware
def call(%Tesla.Env{url: url} = env, next, _) do
- url = Pleroma.HTTP.encode_url(url)
+ url = Pleroma.Utils.URIEncoding.encode_url(url)
env = %{env | url: url}
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index b0aef2592..06d8005bc 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -35,6 +35,7 @@ defmodule Pleroma.Upload do
"""
alias Ecto.UUID
alias Pleroma.Maps
+ alias Pleroma.Utils.URIEncoding
alias Pleroma.Web.ActivityPub.Utils
require Logger
@@ -230,11 +231,18 @@ defmodule Pleroma.Upload do
tmp_path
end
+ # Encoding the whole path here is fine since the path is in a
+ # UUID/ form.
+ # The file at this point isn't %-encoded, so the path shouldn't
+ # be decoded first like Pleroma.Utils.URIEncoding.encode_url/1 does.
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
+ encode_opts = [bypass_decode: true, bypass_parse: true]
+
path =
- URI.encode(path, &char_unescaped?/1) <>
+ URIEncoding.encode_url(path, encode_opts) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
- "?name=#{URI.encode(name, &char_unescaped?/1)}"
+ enum = %{name: name}
+ "?#{URI.encode_query(enum)}"
else
""
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 468e124b5..904e9e056 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -233,8 +233,8 @@ defmodule Pleroma.User do
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do
# `def blocked_users_relation/2`, `def muted_users_relation/2`,
- # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
- # `def subscriber_users/2`, `def endorsed_users_relation/2`
+ # `def reblog_muted_users_relation/2`, `def notification_muted_users_relation/2`,
+ # `def subscriber_users_relation/2`, `def endorsed_users_relation/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target))
@@ -288,6 +288,7 @@ defmodule Pleroma.User do
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
+ defdelegate get_outgoing_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@doc """
@@ -1357,7 +1358,7 @@ defmodule Pleroma.User do
@spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
- if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
+ if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()}$)i, nickname) do
Repo.get_by(User, nickname: local_nickname(nickname))
end
end
@@ -2307,6 +2308,15 @@ defmodule Pleroma.User do
def public_key(_), do: {:error, "key not found"}
+ def get_or_fetch_public_key_for_ap_id(ap_id) do
+ with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
+ {:ok, public_key} <- public_key(user) do
+ {:ok, public_key}
+ else
+ _ -> :error
+ end
+ end
+
def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_cached_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index a7fb8fb83..851745714 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.User.Search do
following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0)
+ capabilities = Keyword.get(opts, :capabilities, [])
for_user = Keyword.get(opts, :for_user)
@@ -32,7 +33,7 @@ defmodule Pleroma.User.Search do
results =
query_string
- |> search_query(for_user, following, top_user_ids)
+ |> search_query(for_user, following, top_user_ids, capabilities)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
results
@@ -80,7 +81,7 @@ defmodule Pleroma.User.Search do
end
end
- defp search_query(query_string, for_user, following, top_user_ids) do
+ defp search_query(query_string, for_user, following, top_user_ids, capabilities) do
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
@@ -94,6 +95,7 @@ defmodule Pleroma.User.Search do
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
+ |> maybe_restrict_accepting_chat_messages(capabilities)
|> filter_deactivated_users()
end
@@ -214,6 +216,14 @@ defmodule Pleroma.User.Search do
end
end
+ defp maybe_restrict_accepting_chat_messages(query, capabilities) do
+ if "accepts_chat_messages" in capabilities do
+ from(q in query, where: q.accepts_chat_messages == true)
+ else
+ query
+ end
+ end
+
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true)
diff --git a/lib/pleroma/utils/uri_encoding.ex b/lib/pleroma/utils/uri_encoding.ex
new file mode 100644
index 000000000..dc6b387fa
--- /dev/null
+++ b/lib/pleroma/utils/uri_encoding.ex
@@ -0,0 +1,142 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2025 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Utils.URIEncoding do
+ @moduledoc """
+ Utility functions for dealing with URI encoding of paths and queries
+ with support for query-encoding quirks.
+ """
+ require Pleroma.Constants
+
+ # We don't always want to decode the path first, like is the case in
+ # Pleroma.Upload.url_from_spec/3.
+ @doc """
+ Wraps URI encoding/decoding functions from Elixir's standard library to fix usually unintended side-effects.
+
+ Supports two URL processing options in the optional 2nd argument with the default being `false`:
+
+ * `bypass_parse` - Bypasses `URI.parse` stage, useful when it's not desirable to parse to URL first
+ before encoding it. Supports only encoding as the Path segment of a URI.
+ * `bypass_decode` - Bypasses `URI.decode` stage for the Path segment of a URI. Used when a URL
+ has to be double %-encoded for internal reasons.
+
+ Options must be specified as a Keyword with tuples with booleans, otherwise
+ `{:error, :invalid_opts}` is returned. Example:
+ `encode_url(url, [bypass_parse: true, bypass_decode: true])`
+ """
+ @spec encode_url(String.t(), Keyword.t()) :: String.t() | {:error, :invalid_opts}
+ def encode_url(url, opts \\ []) when is_binary(url) and is_list(opts) do
+ bypass_parse = Keyword.get(opts, :bypass_parse, false)
+ bypass_decode = Keyword.get(opts, :bypass_decode, false)
+
+ with true <- is_boolean(bypass_parse),
+ true <- is_boolean(bypass_decode) do
+ cond do
+ bypass_parse ->
+ encode_path(url, bypass_decode)
+
+ true ->
+ URI.parse(url)
+ |> then(fn parsed ->
+ path = encode_path(parsed.path, bypass_decode)
+
+ query = encode_query(parsed.query, parsed.host)
+
+ %{parsed | path: path, query: query}
+ end)
+ |> URI.to_string()
+ end
+ else
+ _ -> {:error, :invalid_opts}
+ end
+ end
+
+ defp encode_path(nil, _bypass_decode), do: nil
+
+ # URI.encode/2 deliberately does not encode all chars that are forbidden
+ # in the path component of a URI. It only encodes chars that are forbidden
+ # in the whole URI. A predicate in the 2nd argument is used to fix that here.
+ # URI.encode/2 uses the predicate function to determine whether each byte
+ # (in an integer representation) should be encoded or not.
+ defp encode_path(path, bypass_decode) when is_binary(path) do
+ path =
+ cond do
+ bypass_decode ->
+ path
+
+ true ->
+ URI.decode(path)
+ end
+
+ path
+ |> URI.encode(fn byte ->
+ URI.char_unreserved?(byte) ||
+ Enum.any?(
+ Pleroma.Constants.uri_path_allowed_reserved_chars(),
+ fn char ->
+ char == byte
+ end
+ )
+ end)
+ end
+
+ # Order of kv pairs in query is not preserved when using URI.decode_query.
+ # URI.query_decoder/2 returns a stream which so far appears to not change order.
+ # Immediately switch to a list to prevent breakage for sites that expect
+ # the order of query keys to be always the same.
+ defp encode_query(query, host) when is_binary(query) do
+ query
+ |> URI.query_decoder()
+ |> Enum.to_list()
+ |> do_encode_query(host)
+ end
+
+ defp encode_query(nil, _), do: nil
+
+ # Always uses www_form encoding.
+ # Taken from Elixir's URI module.
+ defp do_encode_query(enumerable, host) do
+ Enum.map_join(enumerable, "&", &maybe_apply_query_quirk(&1, host))
+ end
+
+ # https://git.pleroma.social/pleroma/pleroma/-/issues/1055
+ defp maybe_apply_query_quirk({key, value}, "i.guim.co.uk" = _host) do
+ case key do
+ "precrop" ->
+ query_encode_kv_pair({key, value}, ~c":,")
+
+ key ->
+ query_encode_kv_pair({key, value})
+ end
+ end
+
+ defp maybe_apply_query_quirk({key, value}, _), do: query_encode_kv_pair({key, value})
+
+ # Taken from Elixir's URI module and modified to support quirks.
+ defp query_encode_kv_pair({key, value}, rules \\ []) when is_list(rules) do
+ cond do
+ length(rules) > 0 ->
+ # URI.encode_query/2 does not appear to follow spec and encodes all parts
+ # of our URI path Constant. This appears to work outside of edge-cases
+ # like The Guardian Rich Media Cards, keeping behavior same as with
+ # URI.encode_query/2 unless otherwise specified via rules.
+ (URI.encode_www_form(Kernel.to_string(key)) <>
+ "=" <>
+ URI.encode(value, fn byte ->
+ URI.char_unreserved?(byte) ||
+ Enum.any?(
+ rules,
+ fn char ->
+ char == byte
+ end
+ )
+ end))
+ |> String.replace("%20", "+")
+
+ true ->
+ URI.encode_www_form(Kernel.to_string(key)) <>
+ "=" <> URI.encode_www_form(Kernel.to_string(value))
+ end
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 3e5239a28..edf2e1fa7 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1569,7 +1569,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_actor_url(_url), do: nil
- defp normalize_image(%{"url" => url} = data) do
+ defp normalize_image(%{"url" => url} = data) when is_binary(url) do
%{
"type" => "Image",
"url" => [%{"href" => url}]
@@ -1577,6 +1577,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_put_description(data)
end
+ defp normalize_image(%{"url" => urls}) when is_list(urls) do
+ url = urls |> List.first()
+
+ %{"url" => url}
+ |> normalize_image()
+ end
+
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
index 469d06ef6..51dcd3918 100644
--- a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -18,6 +18,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
content =~ quote_url -> true
# Does the content already have a .quote-inline span?
content =~ "" -> true
+ # Does the content already have a .quote-inline p? (Mastodon)
+ content =~ "" -> true
# No inline quote found
true -> false
end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
index 3e0ac3704..5006167ea 100644
--- a/lib/pleroma/web/api_spec.ex
+++ b/lib/pleroma/web/api_spec.ex
@@ -151,7 +151,8 @@ defmodule Pleroma.Web.ApiSpec do
"Suggestions",
"Announcements",
"Remote interaction",
- "Others"
+ "Others",
+ "Preferred frontends"
]
}
]
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 5a19e0fbb..07eb9328b 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -398,6 +398,28 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ def endorsements_operation do
+ %Operation{
+ tags: ["Retrieve account information"],
+ summary: "Endorsements",
+ description: "Returns endorsed accounts",
+ operationId: "AccountController.endorsements",
+ parameters: [
+ with_relationships_param(),
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Accounts",
+ "application/json",
+ array_of_accounts()
+ ),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
def remove_from_followers_operation do
%Operation{
tags: ["Account actions"],
@@ -461,7 +483,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["follow", "read:mutes"]}],
parameters: [with_relationships_param() | pagination_params()],
responses: %{
- 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ 200 => Operation.response("Accounts", "application/json", array_of_muted_accounts())
}
}
end
@@ -475,7 +497,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:blocks"]}],
parameters: [with_relationships_param() | pagination_params()],
responses: %{
- 200 => Operation.response("Accounts", "application/json", array_of_accounts())
+ 200 => Operation.response("Accounts", "application/json", array_of_blocked_accounts())
}
}
end
@@ -495,16 +517,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
],
responses: %{
200 => Operation.response("Account", "application/json", Account),
+ 401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
- def endorsements_operation do
+ def own_endorsements_operation do
%Operation{
tags: ["Retrieve account information"],
summary: "Endorsements",
- operationId: "AccountController.endorsements",
+ operationId: "AccountController.own_endorsements",
description: "Returns endorsed accounts",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
@@ -874,6 +897,54 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ def array_of_muted_accounts do
+ %Schema{
+ title: "ArrayOfMutedAccounts",
+ type: :array,
+ items: %Schema{
+ title: "MutedAccount",
+ description: "Response schema for a muted account",
+ allOf: [
+ Account,
+ %Schema{
+ type: :object,
+ properties: %{
+ mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
+ }
+ }
+ ]
+ },
+ example: [
+ Account.schema().example
+ |> Map.put("mute_expires_at", "2025-11-29T16:23:13Z")
+ ]
+ }
+ end
+
+ def array_of_blocked_accounts do
+ %Schema{
+ title: "ArrayOfBlockedAccounts",
+ type: :array,
+ items: %Schema{
+ title: "BlockedAccount",
+ description: "Response schema for a blocked account",
+ allOf: [
+ Account,
+ %Schema{
+ type: :object,
+ properties: %{
+ block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
+ }
+ }
+ ]
+ },
+ example: [
+ Account.schema().example
+ |> Map.put("block_expires_at", "2025-11-29T16:23:13Z")
+ ]
+ }
+ end
+
defp array_of_relationships do
%Schema{
title: "ArrayOfRelationships",
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
index f56e57a41..76c2db57e 100644
--- a/lib/pleroma/web/api_spec/operations/chat_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -142,7 +142,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query,
BooleanLike.schema(),
"Include chats from muted users"
- )
+ ),
+ Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
@@ -166,7 +167,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query,
BooleanLike.schema(),
"Include chats from muted users"
- )
+ ),
+ Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
| pagination_params()
],
responses: %{
@@ -257,6 +259,44 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
}
end
+ def pin_operation do
+ %Operation{
+ tags: ["Chats"],
+ summary: "Pin a chat",
+ operationId: "ChatController.pin",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
+ ],
+ responses: %{
+ 200 => Operation.response("The existing chat", "application/json", Chat)
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def unpin_operation do
+ %Operation{
+ tags: ["Chats"],
+ summary: "Unpin a chat",
+ operationId: "ChatController.unpin",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The id of the chat", required: true)
+ ],
+ responses: %{
+ 200 => Operation.response("The existing chat", "application/json", Chat)
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
def chats_response do
%Schema{
title: "ChatsResponse",
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
index b8b37d7cf..b144c2ac0 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -64,25 +64,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
}
end
- def endorsements_operation do
- %Operation{
- tags: ["Retrieve account information"],
- summary: "Endorsements",
- description: "Returns endorsed accounts",
- operationId: "PleromaAPI.AccountController.endorsements",
- parameters: [with_relationships_param(), id_param()],
- responses: %{
- 200 =>
- Operation.response(
- "Array of Accounts",
- "application/json",
- AccountOperation.array_of_accounts()
- ),
- 404 => Operation.response("Not Found", "application/json", ApiError)
- }
- }
- end
-
def subscribe_operation do
%Operation{
deprecated: true,
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex
new file mode 100644
index 000000000..b5a413490
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def outgoing_operation do
+ %Operation{
+ tags: ["Follow requests"],
+ summary: "Retrieve outgoing follow requests",
+ security: [%{"oAuth" => ["read:follows", "follow"]}],
+ operationId: "PleromaFollowRequestController.outgoing",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ })
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex
new file mode 100644
index 000000000..923e4fcc9
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex
@@ -0,0 +1,65 @@
+defmodule Pleroma.Web.ApiSpec.PleromaFrontendSettingsOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(atom) :: Operation.t()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def available_frontends_operation do
+ %Operation{
+ tags: ["Preferred frontends"],
+ summary: "Frontend settings profiles",
+ description: "List frontend setting profiles",
+ operationId: "PleromaAPI.FrontendSettingsController.available_frontends",
+ responses: %{
+ 200 =>
+ Operation.response("Frontends", "application/json", %Schema{
+ type: :array,
+ items: %Schema{
+ type: :string
+ }
+ })
+ }
+ }
+ end
+
+ def update_preferred_frontend_operation do
+ %Operation{
+ tags: ["Preferred frontends"],
+ summary: "Update preferred frontend setting",
+ description: "Store preferred frontend in cookies",
+ operationId: "PleromaAPI.FrontendSettingsController.update_preferred_frontend",
+ requestBody:
+ request_body(
+ "Frontend",
+ %Schema{
+ type: :object,
+ required: [:frontend_name],
+ properties: %{
+ frontend_name: %Schema{
+ type: :string,
+ description: "Frontend name"
+ }
+ }
+ },
+ required: true
+ ),
+ responses: %{
+ 200 =>
+ Operation.response("Preferred frontend", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ frontend_name: %Schema{
+ type: :string,
+ description: "Frontend name"
+ }
+ }
+ })
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
index 6f77584a8..a08b7b580 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
%Operation{
tags: ["Scrobbles"],
summary: "Creates a new Listen activity for an account",
- security: [%{"oAuth" => ["write"]}],
+ security: [%{"oAuth" => ["write:scrobbles"]}],
operationId: "PleromaAPI.ScrobbleController.create",
deprecated: true,
requestBody: request_body("Parameters", create_request(), required: true),
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
],
- security: [%{"oAuth" => ["read"]}],
+ security: [%{"oAuth" => ["read:scrobbles"]}],
responses: %{
200 =>
Operation.response("Array of Scrobble", "application/json", %Schema{
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
index 77c604952..c0919f726 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
@@ -19,7 +19,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do
%Operation{
tags: ["Retrieve status information"],
summary: "Quoted by",
- description: "View quotes for a given status",
+ deprecated: true,
+ description: "View quotes for a given status. Use /api/v1/statuses/:id/quotes instead.",
operationId: "PleromaAPI.StatusController.quotes",
parameters: [id_param() | pagination_params()],
security: [%{"oAuth" => ["read:statuses"]}],
diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex
index c2afe8e18..95d9b063a 100644
--- a/lib/pleroma/web/api_spec/operations/search_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -46,6 +46,12 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
+ ),
+ Operation.parameter(
+ :capabilities,
+ :query,
+ %Schema{type: :array, items: %Schema{type: :string, enum: ["accepts_chat_messages"]}},
+ "Only include accounts with given capabilities"
)
],
responses: %{
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index 02026122f..fed37f51d 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -549,6 +549,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
}
end
+ def quotes_operation do
+ %Operation{
+ tags: ["Retrieve status information"],
+ summary: "Quoted by",
+ description: "View quotes for a given status",
+ operationId: "StatusController.quotes",
+ parameters: [id_param() | pagination_params()],
+ security: [%{"oAuth" => ["read:statuses"]}],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "Array of Status",
+ "application/json",
+ array_of_statuses()
+ ),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
def array_of_statuses do
%Schema{type: :array, items: Status, example: [Status.schema().example]}
end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index 19827e996..7d0b83afe 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -33,8 +33,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
header: %Schema{type: :string, format: :uri},
id: FlakeID,
locked: %Schema{type: :boolean},
- mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
- block_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
index affa25a95..ce08896b5 100644
--- a/lib/pleroma/web/api_spec/schemas/chat.ex
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
account: %Schema{type: :object},
unread: %Schema{type: :integer},
last_message: ChatMessage,
- updated_at: %Schema{type: :string, format: :"date-time"}
+ updated_at: %Schema{type: :string, format: :"date-time"},
+ pinned: %Schema{type: :boolean}
},
example: %{
"account" => %{
@@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"id" => "1",
"unread" => 2,
"last_message" => ChatMessage.schema().example,
- "updated_at" => "2020-04-21T15:06:45.000Z"
+ "updated_at" => "2020-04-21T15:06:45.000Z",
+ "pinned" => false
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index 25548d75b..ac1735e35 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -219,7 +219,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
},
quotes_count: %Schema{
type: :integer,
- description: "How many statuses quoted this status"
+ deprecated: true,
+ description:
+ "How many statuses quoted this status. Deprecated, use `quotes_count` from parent object instead."
},
local: %Schema{
type: :boolean,
@@ -259,6 +261,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}
},
poll: %Schema{allOf: [Poll], nullable: true, description: "The poll attached to the status"},
+ quotes_count: %Schema{
+ type: :integer,
+ description: "How many statuses quoted this status."
+ },
reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true,
@@ -385,6 +391,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"quotes_count" => 0
},
"poll" => nil,
+ "quotes_count" => 0,
"reblog" => nil,
"reblogged" => false,
"reblogs_count" => 0,
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 52c08f00f..91bf9c502 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -402,28 +402,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
- def maybe_notify_subscribers(
- recipients,
- %Activity{data: %{"actor" => actor, "type" => "Create"}} = activity
- ) do
- # Do not notify subscribers if author is making a reply
- with %Object{data: object} <- Object.normalize(activity, fetch: false),
- nil <- object["inReplyTo"],
- %User{} = user <- User.get_cached_by_ap_id(actor) do
- subscriber_ids =
- user
- |> User.subscriber_users()
- |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
- |> Enum.map(& &1.ap_id)
-
- recipients ++ subscriber_ids
- else
- _e -> recipients
- end
- end
-
- def maybe_notify_subscribers(recipients, _), do: recipients
-
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
user
@@ -437,6 +415,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_followers(recipients, _), do: recipients
+ def get_notified_subscribers(
+ %Activity{data: %{"actor" => actor, "type" => "Create"}} = activity
+ ) do
+ # Do not notify subscribers if author is making a reply
+ with %Object{data: object} <- Object.normalize(activity, fetch: false),
+ nil <- object["inReplyTo"],
+ %User{} = user <- User.get_cached_by_ap_id(actor) do
+ subscriber_ids =
+ user
+ |> User.subscriber_users()
+ |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
+ |> Enum.map(& &1.ap_id)
+
+ subscriber_ids
+ else
+ _e -> []
+ end
+ end
+
+ def get_notified_subscribers(_), do: []
+
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index 6637848a9..60fc15b9e 100644
--- a/lib/pleroma/web/fallback/redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end
def redirector(conn, _params, code \\ 200) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
response =
index_content
@@ -51,7 +51,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end
def redirector_with_meta(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
tags = build_tags(conn, params)
preloads = preload_data(conn, params)
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end
def redirector_with_preload(conn, params) do
- {:ok, index_content} = File.read(index_file_path())
+ {:ok, index_content} = File.read(index_file_path(conn))
preloads = preload_data(conn, params)
response =
@@ -91,8 +91,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|> text("")
end
- defp index_file_path do
- Pleroma.Web.Plugs.InstanceStatic.file_path("index.html")
+ defp index_file_path(conn) do
+ frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary)
+
+ Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type)
end
defp build_tags(conn, params) do
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index 304313068..8a52d98e0 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -28,9 +28,12 @@ defmodule Pleroma.Web.Feed.UserController do
ActivityPubController.call(conn, :user)
end
- def feed_redirect(conn, %{"nickname" => nickname}) do
+ def feed_redirect(%{assigns: assigns} = conn, %{"nickname" => nickname}) do
+ format = Map.get(assigns, :format, "atom")
+ format = if format in ["atom", "rss"], do: format, else: "atom"
+
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
- redirect(conn, external: "#{Routes.user_feed_url(conn, :feed, user.nickname)}.atom")
+ redirect(conn, external: "#{Routes.user_feed_url(conn, :feed, user.nickname)}.#{format}")
end
end
diff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex b/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex
new file mode 100644
index 000000000..18752c63c
--- /dev/null
+++ b/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherController do
+ use Pleroma.Web, :controller
+ alias Pleroma.Config
+
+ @doc "GET /frontend_switcher"
+ def switch(conn, _params) do
+ pickable = Config.get([:frontends, :pickable], [])
+
+ conn
+ |> put_view(Pleroma.Web.FrontendSwitcher.FrontendSwitcherView)
+ |> render("switch.html", choices: pickable)
+ end
+
+ @doc "POST /frontend_switcher"
+ def do_switch(conn, params) do
+ conn
+ |> put_resp_cookie("preferred_frontend", params["frontend"])
+ |> html(~s())
+ end
+end
diff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex b/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex
new file mode 100644
index 000000000..284477431
--- /dev/null
+++ b/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex
@@ -0,0 +1,5 @@
+defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherView do
+ use Pleroma.Web, :view
+
+ import Phoenix.HTML.Form
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index d374e8c01..6dc731ed4 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -31,14 +31,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
- plug(:skip_auth when action in [:create, :lookup])
+ plug(:skip_auth when action in [:create])
plug(:skip_public_check when action in [:show, :statuses])
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
- when action in [:show, :followers, :following]
+ when action in [:show, :followers, :following, :lookup, :endorsements]
)
plug(
@@ -50,7 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
- when action in [:verify_credentials, :endorsements]
+ when action in [:verify_credentials, :endorsements, :own_endorsements]
)
plug(
@@ -89,7 +89,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@relationship_actions [:follow, :unfollow, :remove_from_followers]
@needs_account ~W(
followers following lists follow unfollow mute unmute block unblock
- note endorse unendorse remove_from_followers
+ note endorse unendorse endorsements remove_from_followers
)a
plug(
@@ -555,6 +555,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
end
+ @doc "GET /api/v1/accounts/:id/endorsements"
+ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ users =
+ user
+ |> User.endorsed_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Repo.all()
+
+ conn
+ |> render("index.json",
+ for: for_user,
+ users: users,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
+ end
+
@doc "POST /api/v1/accounts/:id/remove_from_followers"
def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"}
@@ -619,8 +635,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/lookup"
- def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
- with %User{} = user <- User.get_by_nickname(nickname) do
+ def lookup(
+ %{assigns: %{user: for_user}, private: %{open_api_spex: %{params: %{acct: nickname}}}} =
+ conn,
+ _params
+ ) do
+ with %User{} = user <- User.get_by_nickname(nickname),
+ :visible <- User.visible_for(user, for_user) do
render(conn, "show.json",
user: user,
skip_visibility_check: true
@@ -631,7 +652,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/endorsements"
- def endorsements(%{assigns: %{user: user}} = conn, params) do
+ def own_endorsements(%{assigns: %{user: user}} = conn, params) do
users =
user
|> User.endorsed_users_relation(_restrict_deactivated = true)
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 53f1216fd..1bfd98eb3 100644
--- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -91,6 +91,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
limit: min(params[:limit], @search_limit),
offset: params[:offset],
type: params[:type],
+ capabilities: params[:capabilities],
author: get_author(params),
embed_relationships: ControllerHelper.embed_relationships?(params),
for_user: user
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 32874d464..5b2cd50bc 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
+ require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Bookmark
@@ -41,7 +42,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:show,
:context,
:show_history,
- :show_source
+ :show_source,
+ :quotes
]
)
@@ -488,6 +490,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
+ |> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^likes, u.ap_id))
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
@@ -523,6 +526,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
+ |> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^announces, u.ap_id))
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
@@ -629,6 +633,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
)
end
+ @doc "GET /api/v1/statuses/:id/quotes"
+ def quotes(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
+ conn,
+ _
+ ) do
+ with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ params =
+ params
+ |> Map.put(:type, "Create")
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:quote_url, object.data["id"])
+
+ recipients =
+ if user do
+ [Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)]
+ else
+ [Pleroma.Constants.as_public()]
+ end
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ else
+ nil -> {:error, :not_found}
+ false -> {:error, :not_found}
+ end
+ end
+
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 1b6f26af7..57372248f 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -146,6 +146,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages",
+ "pleroma:pin_chats",
if Config.get([:instance, :show_reactions]) do
"exposable_reactions"
end,
@@ -257,10 +258,34 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
},
- translation: %{enabled: Pleroma.Language.Translation.configured?()}
+ translation: %{enabled: Pleroma.Language.Translation.configured?()},
+ timelines_access: %{
+ live_feeds: timelines_access(),
+ hashtag_feeds: timelines_access(),
+ # not implemented in Pleroma
+ trending_link_feeds: %{
+ local: "disabled",
+ remote: "disabled"
+ }
+ }
})
end
+ defp timelines_access do
+ %{
+ local: timeline_access(:local),
+ remote: timeline_access(:federated)
+ }
+ end
+
+ defp timeline_access(kind) do
+ if Config.restrict_unauthenticated_access?(:timelines, kind) do
+ "authenticated"
+ else
+ "public"
+ end
+ end
+
defp pleroma_configuration(instance) do
base_urls = %{}
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index c277af98b..27532c42a 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -106,27 +106,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
}
case notification.type do
- "mention" ->
+ type when type in ["mention", "status", "poll"] ->
put_status(response, activity, reading_user, status_render_opts)
- "status" ->
- put_status(response, activity, reading_user, status_render_opts)
-
- "favourite" ->
- put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
-
- "reblog" ->
- put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
-
- "update" ->
+ type when type in ["favourite", "reblog", "update"] ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" ->
put_target(response, activity, reading_user, %{})
- "poll" ->
- put_status(response, activity, reading_user, status_render_opts)
-
"pleroma:emoji_reaction" ->
response
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 4b5ac9c3b..c1a996f62 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -447,6 +447,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
application: build_application(object.data["generator"]),
language: get_language(object),
emojis: build_emojis(object.data["emoji"]),
+ quotes_count: object.data["quotesCount"] || 0,
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
@@ -602,7 +603,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
- href = attachment_url["href"] |> MediaProxy.url()
+ href_remote = attachment_url["href"]
+ href = href_remote |> MediaProxy.url()
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
meta = render("attachment_meta.json", %{attachment: attachment})
@@ -641,7 +643,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{
id: attachment_id,
url: href,
- remote_url: href,
+ remote_url: href_remote,
preview_url: href_preview,
text_url: href,
type: type,
diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex
index 29882542c..f9376b508 100644
--- a/lib/pleroma/web/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
alias Pleroma.Helpers.UriHelper
alias Pleroma.Upload
+ alias Pleroma.Utils.URIEncoding
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy.Invalidation
@@ -99,13 +100,21 @@ defmodule Pleroma.Web.MediaProxy do
{base64, sig64}
end
+ # The URL coming into MediaProxy from the outside might have wrong %-encoding
+ # (like older Pleroma versions).
+ # This would cause an inconsistency with the encoded URL here and the requested
+ # URL fixed with Pleroma.Tesla.Middleware.EncodeUrl.
+ # End result is a failing HEAD request in
+ # Pleroma.Web.MediaProxy.MediaProxyController.handle_preview/2
def encode_url(url) do
+ url = URIEncoding.encode_url(url)
{base64, sig64} = base64_sig64(url)
build_url(sig64, base64, filename(url))
end
def encode_preview_url(url, preview_params \\ []) do
+ url = URIEncoding.encode_url(url)
{base64, sig64} = base64_sig64(url)
build_preview_url(sig64, base64, filename(url), preview_params)
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 85c2393ff..c4e48e83a 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -36,7 +36,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
conn
|> put_resp_header(
"content-type",
- "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
+ "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/#{version}#\"; charset=utf-8"
)
|> json(node_info)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 591391b60..0b8e0b7ad 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -9,7 +9,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
only: [
json_response: 3,
add_link_headers: 2,
- embed_relationships?: 1,
assign_account_by_id: 2
]
@@ -45,12 +44,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
)
- plug(
- OAuthScopesPlug,
- %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
- when action == :endorsements
- )
-
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]} when action == :birthdays
@@ -60,7 +53,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(
:assign_account_by_id
- when action in [:favourites, :endorsements, :subscribe, :unsubscribe]
+ when action in [:favourites, :subscribe, :unsubscribe]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
@@ -109,22 +102,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
)
end
- @doc "GET /api/v1/pleroma/accounts/:id/endorsements"
- def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do
- users =
- user
- |> User.endorsed_users_relation(_restrict_deactivated = true)
- |> Pleroma.Repo.all()
-
- conn
- |> render("index.json",
- for: for_user,
- users: users,
- as: :user,
- embed_relationships: embed_relationships?(params)
- )
- end
-
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, _subscription} <- User.subscribe(user, subscription_target) do
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
index 58780ace2..ac9bb2779 100644
--- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -29,7 +29,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
:create,
:mark_as_read,
:mark_message_as_read,
- :delete_message
+ :delete_message,
+ :pin,
+ :unpin
]
)
@@ -199,8 +201,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
user_id
|> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users)
+ |> restrict_pinned(params)
end
+ defp restrict_pinned(query, %{pinned: pinned}) when is_boolean(pinned) do
+ query
+ |> where([c], c.pinned == ^pinned)
+ end
+
+ defp restrict_pinned(query, _), do: query
+
def create(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
@@ -214,6 +224,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
+ def pin(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
+ {:ok, chat} <- Chat.pin(chat) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ def unpin(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
+ {:ok, chat} <- Chat.unpin(chat) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
defp idempotency_key(conn) do
case get_req_header(conn, "idempotency-key") do
[key] -> key
diff --git a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex
new file mode 100644
index 000000000..656d477da
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.FollowRequestController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]})
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaFollowRequestOperation
+
+ @doc "GET /api/v1/pleroma/outgoing_follow_requests"
+ def outgoing(%{assigns: %{user: follower}} = conn, _params) do
+ follow_requests = User.get_outgoing_follow_requests(follower)
+
+ conn
+ |> put_view(Pleroma.Web.MastodonAPI.FollowRequestView)
+ |> render("index.json", for: follower, users: follow_requests, as: :user)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex b/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex
new file mode 100644
index 000000000..41531c97e
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex
@@ -0,0 +1,37 @@
+defmodule Pleroma.Web.PleromaAPI.FrontendSettingsController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(
+ OAuthScopesPlug,
+ %{fallback: :proceed_unauthenticated, scopes: []}
+ when action in [
+ :available_frontends,
+ :update_preferred_frontend
+ ]
+ )
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaFrontendSettingsOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "GET /api/v1/pleroma/preferred_frontend/available"
+ def available_frontends(conn, _params) do
+ available = Pleroma.Config.get([:frontends, :pickable])
+
+ conn
+ |> json(available)
+ end
+
+ @doc "PUT /api/v1/pleroma/preferred_frontend"
+ def update_preferred_frontend(
+ %{body_params: %{frontend_name: preferred_frontend}} = conn,
+ _params
+ ) do
+ conn
+ |> put_resp_cookie("preferred_frontend", preferred_frontend)
+ |> json(%{frontend_name: preferred_frontend})
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
index 5f5f7643f..207446a84 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -16,10 +16,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
plug(
OAuthScopesPlug,
- %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
+ %{scopes: ["read:scrobbles"], fallback: :proceed_unauthenticated} when action == :index
)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create)
+ plug(OAuthScopesPlug, %{scopes: ["write:scrobbles"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
diff --git a/lib/pleroma/web/pleroma_api/controllers/status_controller.ex b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex
index 482662fdd..c72b2f222 100644
--- a/lib/pleroma/web/pleroma_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex
@@ -5,16 +5,9 @@
defmodule Pleroma.Web.PleromaAPI.StatusController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
-
require Ecto.Query
require Pleroma.Constants
- alias Pleroma.Activity
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@@ -29,38 +22,9 @@ defmodule Pleroma.Web.PleromaAPI.StatusController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaStatusOperation
@doc "GET /api/v1/pleroma/statuses/:id/quotes"
- def quotes(%{assigns: %{user: user}} = conn, %{id: id} = params) do
- with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id),
- true <- Visibility.visible_for_user?(activity, user) do
- params =
- params
- |> Map.put(:type, "Create")
- |> Map.put(:blocking_user, user)
- |> Map.put(:quote_url, object.data["id"])
-
- recipients =
- if user do
- [Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)]
- else
- [Pleroma.Constants.as_public()]
- end
-
- activities =
- recipients
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json",
- activities: activities,
- for: user,
- as: :activity
- )
- else
- nil -> {:error, :not_found}
- false -> {:error, :not_found}
- end
+ def quotes(conn, _params) do
+ conn
+ |> put_view(Pleroma.Web.MastodonAPI.StatusView)
+ |> Pleroma.Web.MastodonAPI.StatusController.call(:quotes)
end
end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
index db6c13c05..d579ca9f6 100644
--- a/lib/pleroma/web/pleroma_api/views/chat_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -24,7 +24,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
last_message:
last_message &&
MessageReferenceView.render("show.json", chat_message_reference: last_message),
- updated_at: Utils.to_masto_date(chat.updated_at)
+ updated_at: Utils.to_masto_date(chat.updated_at),
+ pinned: chat.pinned
}
end
diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex
index 6ab8e4667..f1df185e3 100644
--- a/lib/pleroma/web/plugs/frontend_static.ex
+++ b/lib/pleroma/web/plugs/frontend_static.ex
@@ -5,17 +5,23 @@
defmodule Pleroma.Web.Plugs.FrontendStatic do
require Pleroma.Constants
+ @frontend_cookie_name "preferred_frontend"
+
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
"""
@behaviour Plug
- 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")
+ defp instance_static_path do
+ Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ end
+ def file_path(path, frontend_type \\ :primary)
+
+ def file_path(path, frontend_type) when is_atom(frontend_type) do
+ if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
Path.join([
- instance_static_path,
+ instance_static_path(),
"frontends",
configuration["name"],
configuration["ref"],
@@ -26,6 +32,15 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
end
end
+ def file_path(path, frontend_type) when is_binary(frontend_type) do
+ Path.join([
+ instance_static_path(),
+ "frontends",
+ frontend_type,
+ path
+ ])
+ end
+
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
@@ -36,7 +51,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
def call(conn, opts) do
with false <- api_route?(conn.path_info),
false <- invalid_path?(conn.path_info),
- frontend_type <- Map.get(opts, :frontend_type, :primary),
+ fallback_frontend_type <- Map.get(opts, :frontend_type, :primary),
+ frontend_type <- preferred_or_fallback(conn, fallback_frontend_type),
path when not is_nil(path) <- file_path("", frontend_type) do
call_static(conn, opts, path)
else
@@ -45,6 +61,31 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
end
end
+ def preferred_frontend(conn) do
+ %{req_cookies: cookies} =
+ conn
+ |> Plug.Conn.fetch_cookies()
+
+ Map.get(cookies, @frontend_cookie_name)
+ end
+
+ # Only override primary frontend
+ def preferred_or_fallback(conn, :primary) do
+ case preferred_frontend(conn) do
+ nil ->
+ :primary
+
+ frontend ->
+ if Enum.member?(Pleroma.Config.get([:frontends, :pickable], []), frontend) do
+ frontend
+ else
+ :primary
+ end
+ end
+ end
+
+ def preferred_or_fallback(_conn, fallback), do: fallback
+
defp invalid_path?(list) do
invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
end
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index f82b9a098..d2a674d39 100644
--- a/lib/pleroma/web/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -13,11 +13,11 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
"""
@behaviour Plug
- def file_path(path) do
+ def file_path(path, frontend_type \\ :primary) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
- frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary)
+ frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, frontend_type)
(File.exists?(instance_path) && instance_path) ||
(frontend_path && File.exists?(frontend_path) && frontend_path) ||
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 6fc45bd61..c6fe69e84 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do
end
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
- @supported_alert_types ~w[follow favourite mention reblog poll pleroma:chat_mention pleroma:emoji_reaction]a
+ @supported_alert_types ~w[follow favourite mention status reblog poll pleroma:chat_mention pleroma:emoji_reaction]a
defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index d4be97957..963076510 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -126,6 +126,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end
defp req_headers do
- [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
+ user_agent = Config.get([:rich_media, :user_agent], :default)
+
+ case user_agent do
+ :default -> [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
+ custom -> [{"user-agent", custom}]
+ end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index cd9cfd3ed..7c1d97f63 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -561,6 +561,18 @@ defmodule Pleroma.Web.Router do
get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
+
+ get(
+ "/preferred_frontend/available",
+ FrontendSettingsController,
+ :available_frontends
+ )
+
+ put(
+ "/preferred_frontend",
+ FrontendSettingsController,
+ :update_preferred_frontend
+ )
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
@@ -581,6 +593,8 @@ defmodule Pleroma.Web.Router do
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
+ post("/chats/:id/pin", ChatController, :pin)
+ post("/chats/:id/unpin", ChatController, :unpin)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
@@ -603,12 +617,13 @@ defmodule Pleroma.Web.Router do
post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
+
+ get("/outgoing_follow_requests", FollowRequestController, :outgoing)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
- get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/statuses/:id/quotes", StatusController, :quotes)
end
@@ -637,6 +652,11 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
+ scope "/api/v1/pleroma", Pleroma.Web.MastodonAPI do
+ pipe_through(:api)
+ get("/accounts/:id/endorsements", AccountController, :endorsements)
+ end
+
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
@@ -653,7 +673,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists)
- get("/endorsements", AccountController, :endorsements)
+ get("/endorsements", AccountController, :own_endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
@@ -667,6 +687,8 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse)
+ post("/accounts/:id/endorse", AccountController, :endorse)
+ post("/accounts/:id/unendorse", AccountController, :unendorse)
post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
@@ -742,6 +764,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/statuses/:id/translate", StatusController, :translate)
+ get("/statuses/:id/quotes", StatusController, :quotes)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
@@ -782,6 +805,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
+ get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
@@ -894,7 +918,11 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do
pipe_through(:browser)
+
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
+
+ get("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :switch)
+ post("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :do_switch)
end
pipeline :ap_service_actor do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index cc149e04c..aba42ee78 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Chat.MessageReference
alias Pleroma.Config
alias Pleroma.Conversation.Participation
+ alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
@@ -321,6 +322,16 @@ defmodule Pleroma.Web.Streamer do
end)
end
+ defp do_stream(topic, %Marker{} = marker) do
+ Registry.dispatch(@registry, "#{topic}:#{marker.user_id}", fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ text = StreamerView.render("marker.json", marker)
+
+ send(pid, {:text, text})
+ end)
+ end)
+ end
+
defp do_stream(topic, item) do
Logger.debug("Trying to push to #{topic}")
Logger.debug("Pushing item to #{topic}")
diff --git a/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex b/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex
new file mode 100644
index 000000000..c801c8ee8
--- /dev/null
+++ b/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex
@@ -0,0 +1,7 @@
+
Switch frontend
+
+<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %>
+ <%= select(f, :frontend, @choices) %>
+
+ <%= submit do: "submit" %>
+<% end %>
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 079a37351..5806ba9ab 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.StreamerView do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
+ alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.NotificationView
@@ -164,6 +165,19 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("marker.json", %Marker{} = marker) do
+ %{
+ event: "marker",
+ payload:
+ Pleroma.Web.MastodonAPI.MarkerView.render(
+ "markers.json",
+ markers: [marker]
+ )
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("pleroma_respond.json", %{type: type, result: result} = params) do
%{
event: "pleroma:respond",
diff --git a/priv/repo/migrations/20220222203933_add_pinned_to_chats.exs b/priv/repo/migrations/20220222203933_add_pinned_to_chats.exs
new file mode 100644
index 000000000..2dc461d77
--- /dev/null
+++ b/priv/repo/migrations/20220222203933_add_pinned_to_chats.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddPinnedToChats do
+ use Ecto.Migration
+
+ def change do
+ alter table(:chats) do
+ add(:pinned, :boolean, default: false, null: false)
+ end
+
+ create(index(:chats, [:pinned]))
+ end
+end
diff --git a/priv/repo/migrations/20230703215000_add_accepts_chat_messages_index_to_users.exs b/priv/repo/migrations/20230703215000_add_accepts_chat_messages_index_to_users.exs
new file mode 100644
index 000000000..85a9f27b7
--- /dev/null
+++ b/priv/repo/migrations/20230703215000_add_accepts_chat_messages_index_to_users.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddAcceptsChatMessagesIndexToUsers do
+ use Ecto.Migration
+
+ def change do
+ create(index(:users, [:accepts_chat_messages]))
+ end
+end
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index dad9dc1a1..0defdc74e 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -80,9 +80,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_this_attribute_values(:span, "class", [
"h-card",
"recipients-inline",
- "quote-inline"
+ "quote-inline",
+ "invisible",
+ "ellipsis"
])
+ Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
+
Meta.allow_tag_with_these_attributes(:span, ["lang"])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
diff --git a/priv/static/robots.txt b/priv/static/robots.txt
index 25781b7d7..4559309f4 100644
--- a/priv/static/robots.txt
+++ b/priv/static/robots.txt
@@ -1,2 +1,8 @@
-User-Agent: *
-Disallow:
+User-agent: *
+Disallow: /
+
+User-agent: FediIndex
+Allow: /nodeinfo/2.0
+Allow: /nodeinfo/2.1
+Allow: /nodeinfo/2.2
+Disallow: /
\ No newline at end of file
diff --git a/test/fixtures/quote_post/mastodon_quote_post.json b/test/fixtures/quote_post/mastodon_quote_post.json
new file mode 100644
index 000000000..527a61b91
--- /dev/null
+++ b/test/fixtures/quote_post/mastodon_quote_post.json
@@ -0,0 +1,93 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "sensitive": "as:sensitive",
+ "toot": "http://joinmastodon.org/ns#",
+ "votersCount": "toot:votersCount",
+ "quote": "https://w3id.org/fep/044f#quote",
+ "quoteUri": "http://fedibird.com/ns#quoteUri",
+ "_misskey_quote": "https://misskey-hub.net/ns#_misskey_quote",
+ "quoteAuthorization": {
+ "@id": "https://w3id.org/fep/044f#quoteAuthorization",
+ "@type": "@id"
+ },
+ "gts": "https://gotosocial.org/ns#",
+ "interactionPolicy": {
+ "@id": "gts:interactionPolicy",
+ "@type": "@id"
+ },
+ "canQuote": {
+ "@id": "gts:canQuote",
+ "@type": "@id"
+ },
+ "automaticApproval": {
+ "@id": "gts:automaticApproval",
+ "@type": "@id"
+ },
+ "manualApproval": {
+ "@id": "gts:manualApproval",
+ "@type": "@id"
+ }
+ }
+ ],
+ "id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171",
+ "type": "Note",
+ "summary": null,
+ "inReplyTo": null,
+ "published": "2025-10-09T17:54:47Z",
+ "url": "https://mastodon.social/@gwynnion/115345489087257171",
+ "attributedTo": "https://mastodon.social/users/gwynnion",
+ "to": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "cc": [
+ "https://mastodon.social/users/gwynnion/followers"
+ ],
+ "sensitive": false,
+ "atomUri": "https://mastodon.social/users/gwynnion/statuses/115345489087257171",
+ "inReplyToAtomUri": null,
+ "conversation": "https://mastodon.social/contexts/109836797527169643-115345489087257171",
+ "context": "https://mastodon.social/contexts/109836797527169643-115345489087257171",
+ "content": "RE: https://mastodon.social/@404mediaco/115344945575874225
Every age verification system is just a scheme for companies and hackers to steal your identity.
",
+ "contentMap": {
+ "en": "RE: https://mastodon.social/@404mediaco/115344945575874225
Every age verification system is just a scheme for companies and hackers to steal your identity.
"
+ },
+ "quote": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
+ "_misskey_quote": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
+ "quoteUri": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
+ "quoteAuthorization": "https://mastodon.social/users/404mediaco/quote_authorizations/115345489087269783",
+ "interactionPolicy": {
+ "canQuote": {
+ "automaticApproval": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ]
+ }
+ },
+ "attachment": [],
+ "tag": [],
+ "replies": {
+ "id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies",
+ "type": "Collection",
+ "first": {
+ "type": "CollectionPage",
+ "next": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies?only_other_accounts=true&page=true",
+ "partOf": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies",
+ "items": []
+ }
+ },
+ "likes": {
+ "id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/likes",
+ "type": "Collection",
+ "totalItems": 26
+ },
+ "shares": {
+ "id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/shares",
+ "type": "Collection",
+ "totalItems": 28
+ }
+}
diff --git a/test/fixtures/users_mock/href_as_array.json b/test/fixtures/users_mock/href_as_array.json
new file mode 100644
index 000000000..624612bce
--- /dev/null
+++ b/test/fixtures/users_mock/href_as_array.json
@@ -0,0 +1,41 @@
+{
+ "alsoKnownAs": [],
+ "attachment": [],
+ "capabilities": {},
+ "discoverable": true,
+ "endpoints": {},
+ "featured": "https://queef.in/cute_cat/collections/featured",
+ "followers": "https://queef.in/cute_cat/followers",
+ "following": "https://queef.in/cute_cat/following",
+ "icon": {
+ "type": "Image",
+ "url": [
+ "https://queef.in/storage/profile.webp",
+ "https://example.com/image"
+ ]
+ },
+ "id": "https://queef.in/cute_cat",
+ "image": {
+ "type": "Image",
+ "url": [
+ "https://queef.in/storage/banner.gif",
+ "https://example.com/image"
+ ]
+ },
+ "inbox": "https://queef.in/cute_cat/inbox",
+ "manuallyApprovesFollowers": false,
+ "name": "cute_cat",
+ "outbox": "https://queef.in/cute_cat/outbox",
+ "preferredUsername": "cute_cat",
+ "publicKey": {
+ "id": "https://queef.in/cute_cat#main-key",
+ "owner": "https://queef.in/cute_cat"
+ },
+ "published": "2025-08-18T01:16:10.000Z",
+ "summary": "A cute cat",
+ "tag": [],
+ "type": "Person",
+ "url": "https://queef.in/cute_cat",
+ "vcard:bday": null,
+ "webfinger": "acct:cute_cat@queef.in"
+}
diff --git a/test/pleroma/http_test.exs b/test/pleroma/http_test.exs
index 61347015d..7b6847cf9 100644
--- a/test/pleroma/http_test.exs
+++ b/test/pleroma/http_test.exs
@@ -5,8 +5,11 @@
defmodule Pleroma.HTTPTest do
use ExUnit.Case, async: true
use Pleroma.Tests.Helpers
+
import Tesla.Mock
+
alias Pleroma.HTTP
+ alias Pleroma.Utils.URIEncoding
setup do
mock(fn
@@ -28,6 +31,36 @@ defmodule Pleroma.HTTPTest do
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
%Tesla.Env{status: 200, body: "emoji data"}
+
+ %{
+ method: :get,
+ url: "https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4"
+ } ->
+ %Tesla.Env{status: 200, body: "video data"}
+
+ %{method: :get, url: "https://example.com/media/unicode%20%F0%9F%99%82%20.gif"} ->
+ %Tesla.Env{status: 200, body: "unicode data"}
+
+ %{
+ method: :get,
+ url:
+ "https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
+ } ->
+ %Tesla.Env{status: 200, body: "Guardian image quirk"}
+
+ %{
+ method: :get,
+ url:
+ "https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
+ } ->
+ %Tesla.Env{status: 200, body: "Space in query with Guardian quirk"}
+
+ %{
+ method: :get,
+ url:
+ "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
+ } ->
+ %Tesla.Env{status: 200, body: "AWS S3 data"}
end)
:ok
@@ -85,5 +118,100 @@ defmodule Pleroma.HTTPTest do
{:ok, result} = HTTP.get(properly_encoded_url)
assert result.status == 200
+
+ url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
+
+ {:ok, result} = HTTP.get(url_with_reserved_chars)
+
+ assert result.status == 200
+
+ url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
+
+ {:ok, result} = HTTP.get(url_with_unicode)
+
+ assert result.status == 200
+ end
+
+ test "decodes URL first by default" do
+ clear_config(:test_url_encoding, true)
+
+ normal_url = "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
+
+ result = URIEncoding.encode_url(normal_url)
+
+ assert result == "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
+ end
+
+ test "doesn't decode URL first when specified" do
+ clear_config(:test_url_encoding, true)
+
+ normal_url = "https://example.com/media/file%20with%20space.jpg"
+
+ result = URIEncoding.encode_url(normal_url, bypass_decode: true)
+
+ assert result == "https://example.com/media/file%2520with%2520space.jpg"
+ end
+
+ test "properly applies Guardian image query quirk" do
+ clear_config(:test_url_encoding, true)
+
+ url =
+ "https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
+
+ result = URIEncoding.encode_url(url)
+
+ assert result == url
+
+ {:ok, result_get} = HTTP.get(result)
+
+ assert result_get.status == 200
+ end
+
+ test "properly encodes spaces as \"pluses\" in query when using quirks" do
+ clear_config(:test_url_encoding, true)
+
+ url =
+ "https://i.guim.co.uk/emoji/Pack 1/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar baz"
+
+ properly_encoded_url =
+ "https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
+
+ result = URIEncoding.encode_url(url)
+
+ assert result == properly_encoded_url
+
+ {:ok, result_get} = HTTP.get(result)
+
+ assert result_get.status == 200
+ end
+
+ test "properly encode AWS S3 queries" do
+ clear_config(:test_url_encoding, true)
+
+ url =
+ "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
+
+ unencoded_url =
+ "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY/20130721/us-east-1/s3/aws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
+
+ result = URIEncoding.encode_url(url)
+ result_unencoded = URIEncoding.encode_url(unencoded_url)
+
+ assert result == url
+ assert result == result_unencoded
+
+ {:ok, result_get} = HTTP.get(result)
+
+ assert result_get.status == 200
+ end
+
+ test "preserves query key order" do
+ clear_config(:test_url_encoding, true)
+
+ url = "https://example.com/foo?hjkl=qwertz&xyz=abc&bar=baz"
+
+ result = URIEncoding.encode_url(url)
+
+ assert result == url
end
end
diff --git a/test/pleroma/language/translation/translate_locally_test.exs b/test/pleroma/language/translation/translate_locally_test.exs
new file mode 100644
index 000000000..51cbd11bd
--- /dev/null
+++ b/test/pleroma/language/translation/translate_locally_test.exs
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Language.Translation.TranslateLocallyTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Language.Translation.TranslateLocally
+
+ @example_models %{
+ "de" => %{
+ "en" => "de-en-base"
+ },
+ "en" => %{
+ "de" => "en-de-base",
+ "pl" => "en-pl-tiny"
+ },
+ "cs" => %{
+ "en" => "cs-en-base"
+ },
+ "pl" => %{
+ "en" => "pl-en-tiny"
+ }
+ }
+
+ test "it returns languages list" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+
+ assert {:ok, languages} = TranslateLocally.supported_languages(:source)
+ assert ["cs", "de", "en", "pl"] = languages |> Enum.sort()
+ end
+
+ describe "it returns languages matrix" do
+ test "without intermediary language" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+
+ assert {:ok,
+ %{
+ "cs" => ["en"],
+ "de" => ["en"],
+ "en" => ["de", "pl"],
+ "pl" => ["en"]
+ }} = TranslateLocally.languages_matrix()
+ end
+
+ test "with intermediary language" do
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
+ clear_config([Pleroma.Language.Translation.TranslateLocally, :intermediary_language], "en")
+
+ assert {:ok,
+ %{
+ "cs" => ["de", "en", "pl"],
+ "de" => ["en", "pl"],
+ "en" => ["de", "pl"],
+ "pl" => ["de", "en"]
+ }} = TranslateLocally.languages_matrix()
+ end
+ end
+end
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index 4b20e07cf..a2be2ae49 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -17,6 +17,7 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
+ alias Pleroma.Web.Streamer
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
@@ -446,8 +447,7 @@ defmodule Pleroma.NotificationTest do
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
- user = insert(:user)
- other_user = insert(:user)
+ [user, other_user] = insert_pair(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
@@ -486,6 +486,37 @@ defmodule Pleroma.NotificationTest do
assert m.last_read_id == to_string(n2.id)
end
+
+ @tag needs_streamer: true
+ test "it sends updated marker to the 'user' and the 'user:notification' stream" do
+ %{user: user, token: oauth_token} = oauth_access(["read"])
+ other_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(other_user, %{
+ status: "hi @#{user.nickname}!"
+ })
+
+ [%{id: notification_id}] = Notification.for_user(user)
+
+ notification_id = to_string(notification_id)
+
+ task =
+ Task.async(fn ->
+ {:ok, _topic} =
+ Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
+
+ assert_receive {:text, event}, 4_000
+
+ assert %{"event" => "marker", "payload" => payload} = Jason.decode!(event)
+
+ assert %{"notifications" => %{"last_read_id" => ^notification_id}} =
+ Jason.decode!(payload)
+ end)
+
+ Notification.set_read_up_to(user, notification_id)
+ Task.await(task)
+ end
end
describe "for_user_since/2" do
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
index 034ab28a5..8dbe9c6bf 100644
--- a/test/pleroma/reverse_proxy_test.exs
+++ b/test/pleroma/reverse_proxy_test.exs
@@ -396,18 +396,29 @@ defmodule Pleroma.ReverseProxyTest do
end
end
- # Hackey is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
+ # Hackney is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
# Gun is able to proxy through Tesla, so it does not need testing as the
# test cases in the Pleroma.HTTPTest module are sufficient
describe "Hackney URL encoding:" do
setup do
ClientMock
- |> expect(:request, fn :get,
- "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz",
- _headers,
- _body,
- _opts ->
- {:ok, 200, [{"content-type", "image/png"}], "It works!"}
+ |> expect(:request, fn
+ :get,
+ "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz",
+ _headers,
+ _body,
+ _opts ->
+ {:ok, 200, [{"content-type", "image/png"}], "It works!"}
+
+ :get,
+ "https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4",
+ _headers,
+ _body,
+ _opts ->
+ {:ok, 200, [{"content-type", "video/mp4"}], "Allowed reserved chars."}
+
+ :get, "https://example.com/media/unicode%20%F0%9F%99%82%20.gif", _headers, _body, _opts ->
+ {:ok, 200, [{"content-type", "image/gif"}], "Unicode emoji in path"}
end)
|> stub(:stream_body, fn _ -> :done end)
|> stub(:close, fn _ -> :ok end)
@@ -430,5 +441,21 @@ defmodule Pleroma.ReverseProxyTest do
assert result.status == 200
end
+
+ test "properly encodes URLs with allowed reserved characters", %{conn: conn} do
+ url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
+
+ result = ReverseProxy.call(conn, url_with_reserved_chars)
+
+ assert result.status == 200
+ end
+
+ test "properly encodes URLs with unicode in path", %{conn: conn} do
+ url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
+
+ result = ReverseProxy.call(conn, url_with_unicode)
+
+ assert result.status == 200
+ end
end
end
diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs
index 5fd62fa43..9a51c612b 100644
--- a/test/pleroma/upload_test.exs
+++ b/test/pleroma/upload_test.exs
@@ -227,20 +227,35 @@ defmodule Pleroma.UploadTest do
assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg"
end
- test "escapes reserved uri characters" do
+ test "escapes disallowed reserved characters in uri path" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
- filename: ":?#[]@!$&\\'()*+,;=.jpg"
+ filename: ":?#[]@!$&'()*+,;=.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) ==
- "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
+ ":%3F%23%5B%5D@!$&'()*+,;=.jpg"
+ end
+
+ test "double %-encodes filename" do
+ File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+ file = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image_tmp.jpg"),
+ filename: "file with %20.jpg"
+ }
+
+ {:ok, data} = Upload.store(file)
+ [attachment_url | _] = data["url"]
+
+ assert Path.basename(attachment_url["href"]) == "file%20with%20%2520.jpg"
end
end
@@ -267,4 +282,23 @@ defmodule Pleroma.UploadTest do
refute String.starts_with?(url, base_url <> "/media/")
end
end
+
+ describe "Setting a link_name for uploaded media" do
+ setup do: clear_config([Pleroma.Upload, :link_name], true)
+
+ test "encodes name parameter in query" do
+ File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+ file = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image_tmp.jpg"),
+ filename: "test file.jpg"
+ }
+
+ {:ok, data} = Upload.store(file)
+ [attachment_url | _] = data["url"]
+
+ assert Path.basename(attachment_url["href"]) == "test%20file.jpg?name=test+file.jpg"
+ end
+ end
end
diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs
index 1af9a1493..75cfbd2db 100644
--- a/test/pleroma/user_search_test.exs
+++ b/test/pleroma/user_search_test.exs
@@ -366,5 +366,13 @@ defmodule Pleroma.UserSearchTest do
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
+
+ test "find users accepting chat messages only" do
+ user1 = insert(:user, nickname: "user1", accepts_chat_messages: true)
+ insert(:user, nickname: "user2", accepts_chat_messages: false)
+
+ [found_user1] = User.search("user", capabilities: ["accepts_chat_messages"])
+ assert found_user1.id == user1.id
+ end
end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 0b4dc9197..b2533e9f1 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -1881,6 +1881,11 @@ defmodule Pleroma.UserTest do
end
end
+ test "get_or_fetch_public_key_for_ap_id fetches a user that's not in the db" do
+ assert {:ok, _key} =
+ User.get_or_fetch_public_key_for_ap_id("http://mastodon.example.org/users/admin")
+ end
+
test "get_public_key_for_ap_id returns correctly for user that's not in the db" do
assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
index 6c2a6ccf8..73f53db56 100644
--- a/test/pleroma/web/activity_pub/activity_pub_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -465,6 +465,40 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
+ test "works with avatar/banner href as list" do
+ user_id = "https://queef.in/cute_cat"
+
+ user_data =
+ "test/fixtures/users_mock/href_as_array.json"
+ |> File.read!()
+ |> Jason.decode!()
+ |> Map.delete("featured")
+ |> Jason.encode!()
+
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: ^user_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: user_data,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+ end)
+
+ {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+
+ assert length(user.avatar["url"]) == 1
+ assert length(user.banner["url"]) == 1
+
+ assert user.avatar["url"] |> List.first() |> Map.fetch!("href") ==
+ "https://queef.in/storage/profile.webp"
+
+ assert user.banner["url"] |> List.first() |> Map.fetch!("href") ==
+ "https://queef.in/storage/banner.gif"
+ end
+
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
index d5762766f..c03787abb 100644
--- a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs
@@ -109,4 +109,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
{:ok, filtered} = InlineQuotePolicy.filter(activity)
assert filtered == activity
end
+
+ # Mastodon uses p tags instead of span in their quote posts
+ # URLs in quoteUri and post content are already mismatched
+ test "skips objects which already have an .inline-quote p" do
+ object = File.read!("test/fixtures/quote_post/mastodon_quote_post.json") |> Jason.decode!()
+
+ # Normally the ObjectValidator will fix this before it reaches MRF
+ object = Map.put(object, "quoteUrl", object["quoteUri"])
+
+ activity = %{
+ "type" => "Create",
+ "actor" => "https://mastodon.social/users/gwynnion",
+ "object" => object
+ }
+
+ {:ok, filtered} = InlineQuotePolicy.filter(activity)
+ assert filtered == activity
+ end
end
diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs
index 0a3aaff5c..916531cd5 100644
--- a/test/pleroma/web/feed/user_controller_test.exs
+++ b/test/pleroma/web/feed/user_controller_test.exs
@@ -282,6 +282,21 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
"#{Pleroma.Web.Endpoint.url()}/users/#{user.nickname}/feed.atom"
end
+ test "redirects to rss feed when explicitly requested", %{conn: conn} do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ conn =
+ conn
+ |> put_req_header("accept", "application/xml")
+ |> get("/users/#{user.nickname}.rss")
+
+ assert conn.status == 302
+
+ assert redirected_to(conn) ==
+ "#{Pleroma.Web.Endpoint.url()}/users/#{user.nickname}/feed.rss"
+ end
+
test "with non-html / non-json format, it returns error when user is not found", %{conn: conn} do
response =
conn
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index 2d91964da..02da781dd 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -2104,6 +2104,50 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|> json_response_and_validate_schema(404)
end
+ test "account lookup with restrict unauthenticated profiles for local" do
+ clear_config([:restrict_unauthenticated, :profiles, :local], true)
+
+ user = insert(:user, local: true)
+ reading_user = insert(:user)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
+
+ assert json_response_and_validate_schema(conn, 401)
+
+ conn =
+ build_conn()
+ |> assign(:user, reading_user)
+ |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
+
+ assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
+ assert id == user.id
+ end
+
+ test "account lookup with restrict unauthenticated profiles for remote" do
+ clear_config([:restrict_unauthenticated, :profiles, :remote], true)
+
+ user = insert(:user, nickname: "user@example.com", local: false)
+ reading_user = insert(:user)
+
+ conn =
+ build_conn()
+ |> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
+
+ assert json_response_and_validate_schema(conn, 401)
+
+ conn =
+ build_conn()
+ |> assign(:user, reading_user)
+ |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
+ |> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
+
+ assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
+ assert id == user.id
+ end
+
test "create a note on a user" do
%{conn: conn} = oauth_access(["write:accounts", "read:follows"])
other_user = insert(:user)
@@ -2134,7 +2178,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert %{"id" => ^id1, "endorsed" => true} =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/v1/accounts/#{id1}/pin")
+ |> post("/api/v1/accounts/#{id1}/endorse")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] =
@@ -2153,7 +2197,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert %{"id" => ^id1, "endorsed" => false} =
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/v1/accounts/#{id1}/unpin")
+ |> post("/api/v1/accounts/#{id1}/unendorse")
|> json_response_and_validate_schema(200)
assert [] =
@@ -2172,15 +2216,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
conn
|> put_req_header("content-type", "application/json")
- |> post("/api/v1/accounts/#{id1}/pin")
+ |> post("/api/v1/accounts/#{id1}/endorse")
|> json_response_and_validate_schema(200)
assert %{"error" => "You have already pinned the maximum number of users"} =
conn
|> assign(:user, user)
- |> post("/api/v1/accounts/#{id2}/pin")
+ |> post("/api/v1/accounts/#{id2}/endorse")
|> json_response_and_validate_schema(400)
end
+
+ test "returns a list of pinned accounts", %{conn: conn} do
+ clear_config([:instance, :max_endorsed_users], 3)
+
+ %{id: id1} = user1 = insert(:user)
+ %{id: id2} = user2 = insert(:user)
+ %{id: id3} = user3 = insert(:user)
+
+ CommonAPI.follow(user2, user1)
+ CommonAPI.follow(user3, user1)
+
+ User.endorse(user1, user2)
+ User.endorse(user1, user3)
+
+ [%{"id" => ^id2}, %{"id" => ^id3}] =
+ conn
+ |> get("/api/v1/accounts/#{id1}/endorsements")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "returns 404 error when specified user is not exist", %{conn: conn} do
+ conn = get(conn, "/api/v1/accounts/test/endorsements")
+
+ assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
+ end
end
describe "familiar followers" do
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
index 8a0fe5259..10c0b6ea7 100644
--- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -194,4 +194,28 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy")
refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "upload")
end
+
+ test "display timeline access restrictions", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines, :local], true)
+ clear_config([:restrict_unauthenticated, :timelines, :federated], false)
+
+ conn = get(conn, "/api/v2/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+
+ assert result["configuration"]["timelines_access"] == %{
+ "live_feeds" => %{
+ "local" => "authenticated",
+ "remote" => "public"
+ },
+ "hashtag_feeds" => %{
+ "local" => "authenticated",
+ "remote" => "public"
+ },
+ "trending_link_feeds" => %{
+ "local" => "disabled",
+ "remote" => "disabled"
+ }
+ }
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index 25a17d5c1..5c24df864 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -1867,18 +1867,29 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
%{activity: activity}
end
- test "returns users who have favorited the status", %{conn: conn, activity: activity} do
- other_user = insert(:user)
- {:ok, _} = CommonAPI.favorite(activity.id, other_user)
+ test "returns users who have favorited the status ordered from newest to oldest", %{
+ conn: conn,
+ activity: activity
+ } do
+ [other_user_1, other_user_2] = insert_pair(:user)
+ [other_user_3, other_user_4] = insert_pair(:user)
+
+ {:ok, _} = CommonAPI.favorite(activity.id, other_user_1)
+ {:ok, _} = CommonAPI.favorite(activity.id, other_user_3)
+ {:ok, _} = CommonAPI.favorite(activity.id, other_user_2)
+ {:ok, _} = CommonAPI.favorite(activity.id, other_user_4)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
- [%{"id" => id}] = response
+ [%{"id" => id1}, %{"id" => id2}, %{"id" => id3}, %{"id" => id4}] = response
- assert id == other_user.id
+ assert id1 == other_user_4.id
+ assert id2 == other_user_2.id
+ assert id3 == other_user_3.id
+ assert id4 == other_user_1.id
end
test "returns empty array when status has not been favorited yet", %{
@@ -2541,4 +2552,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(404)
end
end
+
+ describe "getting quotes of a specified post" do
+ setup do
+ [current_user, user] = insert_pair(:user)
+ %{user: current_user, conn: conn} = oauth_access(["read:statuses"], user: current_user)
+ [current_user: current_user, user: user, conn: conn]
+ end
+
+ test "shows quotes of a post", %{conn: conn} do
+ user = insert(:user)
+ activity = insert(:note_activity)
+
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quote_id: activity.id})
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/quotes")
+ |> json_response_and_validate_schema(:ok)
+
+ [status] = response
+
+ assert length(response) == 1
+ assert status["id"] == quote_post.id
+ end
+
+ test "returns 404 error when a post can't be seen", %{conn: conn} do
+ activity = insert(:direct_note_activity)
+
+ response =
+ conn
+ |> get("/api/v1/statuses/#{activity.id}/quotes")
+
+ assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"}
+ end
+
+ test "returns 404 error when a post does not exist", %{conn: conn} do
+ response =
+ conn
+ |> get("/api/v1/statuses/idontexist/quotes")
+
+ assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"}
+ end
+ end
end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index 863e0349c..d7908886b 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -344,7 +344,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
quotes_count: 0,
bookmark_folder: nil,
list_id: nil
- }
+ },
+ quotes_count: 0
}
assert status == expected
diff --git a/test/pleroma/web/media_proxy_test.exs b/test/pleroma/web/media_proxy_test.exs
index 718892665..86d30c04c 100644
--- a/test/pleroma/web/media_proxy_test.exs
+++ b/test/pleroma/web/media_proxy_test.exs
@@ -73,7 +73,7 @@ defmodule Pleroma.Web.MediaProxyTest do
end
test "encodes and decodes URL and ignores query params for the path" do
- url = "https://pleroma.soykaf.com/static/logo.png?93939393939&bunny=true"
+ url = "https://pleroma.soykaf.com/static/logo.png?93939393939=&bunny=true"
encoded = MediaProxy.url(url)
assert String.ends_with?(encoded, "/logo.png")
assert decode_result(encoded) == url
@@ -159,18 +159,6 @@ defmodule Pleroma.Web.MediaProxyTest do
assert String.starts_with?(encoded, base_url)
end
- # Some sites expect ASCII encoded characters in the URL to be preserved even if
- # unnecessary.
- # Issues: https://git.pleroma.social/pleroma/pleroma/issues/580
- # https://git.pleroma.social/pleroma/pleroma/issues/1055
- test "preserve ASCII encoding" do
- url =
- "https://pleroma.com/%20/%21/%22/%23/%24/%25/%26/%27/%28/%29/%2A/%2B/%2C/%2D/%2E/%2F/%30/%31/%32/%33/%34/%35/%36/%37/%38/%39/%3A/%3B/%3C/%3D/%3E/%3F/%40/%41/%42/%43/%44/%45/%46/%47/%48/%49/%4A/%4B/%4C/%4D/%4E/%4F/%50/%51/%52/%53/%54/%55/%56/%57/%58/%59/%5A/%5B/%5C/%5D/%5E/%5F/%60/%61/%62/%63/%64/%65/%66/%67/%68/%69/%6A/%6B/%6C/%6D/%6E/%6F/%70/%71/%72/%73/%74/%75/%76/%77/%78/%79/%7A/%7B/%7C/%7D/%7E/%7F/%80/%81/%82/%83/%84/%85/%86/%87/%88/%89/%8A/%8B/%8C/%8D/%8E/%8F/%90/%91/%92/%93/%94/%95/%96/%97/%98/%99/%9A/%9B/%9C/%9D/%9E/%9F/%C2%A0/%A1/%A2/%A3/%A4/%A5/%A6/%A7/%A8/%A9/%AA/%AB/%AC/%C2%AD/%AE/%AF/%B0/%B1/%B2/%B3/%B4/%B5/%B6/%B7/%B8/%B9/%BA/%BB/%BC/%BD/%BE/%BF/%C0/%C1/%C2/%C3/%C4/%C5/%C6/%C7/%C8/%C9/%CA/%CB/%CC/%CD/%CE/%CF/%D0/%D1/%D2/%D3/%D4/%D5/%D6/%D7/%D8/%D9/%DA/%DB/%DC/%DD/%DE/%DF/%E0/%E1/%E2/%E3/%E4/%E5/%E6/%E7/%E8/%E9/%EA/%EB/%EC/%ED/%EE/%EF/%F0/%F1/%F2/%F3/%F4/%F5/%F6/%F7/%F8/%F9/%FA/%FB/%FC/%FD/%FE/%FF"
-
- encoded = MediaProxy.url(url)
- assert decode_result(encoded) == url
- end
-
# This includes unsafe/reserved characters which are not interpreted as part of the URL
# and would otherwise have to be ASCII encoded. It is our role to ensure the proxied URL
# is unmodified, so we are testing these characters anyway.
@@ -182,11 +170,30 @@ defmodule Pleroma.Web.MediaProxyTest do
assert decode_result(encoded) == url
end
- test "preserve unicode characters" do
+ # Improperly encoded URLs should not happen even when input was wrong.
+ test "does not preserve unicode characters" do
url = "https://ko.wikipedia.org/wiki/위키백과:대문"
+ encoded_url =
+ "https://ko.wikipedia.org/wiki/%EC%9C%84%ED%82%A4%EB%B0%B1%EA%B3%BC:%EB%8C%80%EB%AC%B8"
+
encoded = MediaProxy.url(url)
- assert decode_result(encoded) == url
+ assert decode_result(encoded) == encoded_url
+ end
+
+ # If we preserve wrongly encoded URLs in MediaProxy, it will get fixed
+ # when we GET these URLs and will result in 424 when MediaProxy previews are enabled.
+ test "does not preserve incorrect URLs when making MediaProxy link" do
+ incorrect_original_url = "https://example.com/media/cofe%20%28with%20milk%29.png"
+ corrected_original_url = "https://example.com/media/cofe%20(with%20milk).png"
+
+ unpreserved_encoded_original_url =
+ "http://localhost:4001/proxy/Sv6tt6xjA72_i4d8gXbuMAOXQSs/aHR0cHM6Ly9leGFtcGxlLmNvbS9tZWRpYS9jb2ZlJTIwKHdpdGglMjBtaWxrKS5wbmc/cofe%20(with%20milk).png"
+
+ encoded = MediaProxy.url(incorrect_original_url)
+
+ assert encoded == unpreserved_encoded_original_url
+ assert decode_result(encoded) == corrected_original_url
end
end
diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
index d152a44cd..38a928f30 100644
--- a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs
@@ -280,35 +280,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
end
end
- describe "account endorsements" do
- test "returns a list of pinned accounts", %{conn: conn} do
- %{id: id1} = user1 = insert(:user)
- %{id: id2} = user2 = insert(:user)
- %{id: id3} = user3 = insert(:user)
-
- CommonAPI.follow(user2, user1)
- CommonAPI.follow(user3, user1)
-
- User.endorse(user1, user2)
- User.endorse(user1, user3)
-
- response =
- conn
- |> get("/api/v1/pleroma/accounts/#{id1}/endorsements")
- |> json_response_and_validate_schema(200)
-
- assert length(response) == 2
- assert Enum.any?(response, fn user -> user["id"] == id2 end)
- assert Enum.any?(response, fn user -> user["id"] == id3 end)
- end
-
- test "returns 404 error when specified user is not exist", %{conn: conn} do
- conn = get(conn, "/api/v1/pleroma/accounts/test/endorsements")
-
- assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
- end
- end
-
describe "birthday reminders" do
test "returns a list of friends having birthday on specified day" do
%{user: user, conn: conn} = oauth_access(["read:accounts"])
diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs
index 0d3452559..0dc0e7014 100644
--- a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs
@@ -337,6 +337,41 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
end
end
+ describe "POST /api/v1/pleroma/chats/:id/pin" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it pins a chat", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> post("/api/v1/pleroma/chats/#{chat.id}/pin")
+ |> json_response_and_validate_schema(200)
+
+ assert %{"pinned" => true} = result
+ end
+ end
+
+ describe "POST /api/v1/pleroma/chats/:id/unpin" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it unpins a chat", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+ {:ok, chat} = Chat.pin(chat)
+
+ result =
+ conn
+ |> post("/api/v1/pleroma/chats/#{chat.id}/unpin")
+ |> json_response_and_validate_schema(200)
+
+ assert %{"pinned" => false} = result
+ end
+ end
+
for tested_endpoint <- ["/api/v1/pleroma/chats", "/api/v2/pleroma/chats"] do
describe "GET #{tested_endpoint}" do
setup do: oauth_access(["read:chats"])
@@ -407,6 +442,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
assert length(result) == 1
end
+ test "it only returns pinned chats", %{conn: conn, user: user} do
+ recipient1 = insert(:user)
+ recipient2 = insert(:user)
+
+ {:ok, %{id: id} = chat} = Chat.get_or_create(user.id, recipient1.ap_id)
+ {:ok, _} = Chat.get_or_create(user.id, recipient2.ap_id)
+
+ Chat.pin(chat)
+
+ [%{"id" => ^id, "pinned" => true}] =
+ conn
+ |> get("#{unquote(tested_endpoint)}?pinned=true")
+ |> json_response_and_validate_schema(200)
+ end
+
if tested_endpoint == "/api/v1/pleroma/chats" do
test "it returns all chats", %{conn: conn, user: user} do
Enum.each(1..30, fn _ ->
diff --git a/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs
new file mode 100644
index 000000000..46109e35e
--- /dev/null
+++ b/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.FollowRequestControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "/api/v1/pleroma/outgoing_follow_requests works" do
+ %{conn: conn, user: user} = oauth_access(["read:follows"])
+
+ other_user1 = insert(:user)
+ other_user2 = insert(:user, is_locked: true)
+ _other_user3 = insert(:user)
+
+ {:ok, _, _, _} = CommonAPI.follow(other_user1, user)
+ {:ok, _, _, _} = CommonAPI.follow(other_user2, user)
+
+ conn = get(conn, "/api/v1/pleroma/outgoing_follow_requests")
+
+ assert [relationship] = json_response_and_validate_schema(conn, 200)
+ assert to_string(other_user2.id) == relationship["id"]
+ end
+end
diff --git a/test/pleroma/web/pleroma_api/controllers/frontend_settings_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/frontend_settings_controller_test.exs
new file mode 100644
index 000000000..1f14c102f
--- /dev/null
+++ b/test/pleroma/web/pleroma_api/controllers/frontend_settings_controller_test.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Web.PleromaAPI.FrontendSettingsControllerTest do
+ use Pleroma.Web.ConnCase, async: false
+
+ describe "PUT /api/v1/pleroma/preferred_frontend" do
+ test "sets a cookie with selected frontend" do
+ %{conn: conn} = oauth_access(["read"])
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/v1/pleroma/preferred_frontend", %{"frontend_name" => "pleroma-fe/stable"})
+
+ json_response_and_validate_schema(response, 200)
+ assert %{"preferred_frontend" => %{value: "pleroma-fe/stable"}} = response.resp_cookies
+ end
+ end
+end
diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
index bcc25b83e..fd405d7d7 100644
--- a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
describe "POST /api/v1/pleroma/scrobble" do
test "works correctly" do
- %{conn: conn} = oauth_access(["write"])
+ %{conn: conn} = oauth_access(["write:scrobbles"])
conn =
conn
@@ -51,7 +51,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do
describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do
test "works correctly" do
- %{user: user, conn: conn} = oauth_access(["read"])
+ %{user: user, conn: conn} = oauth_access(["read:scrobbles"])
{:ok, _activity} =
CommonAPI.listen(user, %{
diff --git a/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs
index a24e98a07..4b4418384 100644
--- a/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs
@@ -9,46 +9,22 @@ defmodule Pleroma.Web.PleromaAPI.StatusControllerTest do
import Pleroma.Factory
- describe "getting quotes of a specified post" do
- setup do
- [current_user, user] = insert_pair(:user)
- %{user: current_user, conn: conn} = oauth_access(["read:statuses"], user: current_user)
- [current_user: current_user, user: user, conn: conn]
- end
+ test "/quotes fallback works" do
+ [current_user, user] = insert_pair(:user)
+ %{conn: conn} = oauth_access(["read:statuses"], user: current_user)
- test "shows quotes of a post", %{conn: conn} do
- user = insert(:user)
- activity = insert(:note_activity)
+ activity = insert(:note_activity)
- {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quoted_status_id: activity.id})
+ {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quoted_status_id: activity.id})
- response =
- conn
- |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes")
- |> json_response_and_validate_schema(:ok)
+ response =
+ conn
+ |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes")
+ |> json_response_and_validate_schema(:ok)
- [status] = response
+ [status] = response
- assert length(response) == 1
- assert status["id"] == quote_post.id
- end
-
- test "returns 404 error when a post can't be seen", %{conn: conn} do
- activity = insert(:direct_note_activity)
-
- response =
- conn
- |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes")
-
- assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"}
- end
-
- test "returns 404 error when a post does not exist", %{conn: conn} do
- response =
- conn
- |> get("/api/v1/pleroma/statuses/idontexist/quotes")
-
- assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"}
- end
+ assert length(response) == 1
+ assert status["id"] == quote_post.id
end
end
diff --git a/test/pleroma/web/pleroma_api/views/chat_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_view_test.exs
index 405f1b6b7..d7efe8dfe 100644
--- a/test/pleroma/web/pleroma_api/views/chat_view_test.exs
+++ b/test/pleroma/web/pleroma_api/views/chat_view_test.exs
@@ -30,7 +30,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
AccountView.render("show.json", user: recipient, skip_visibility_check: true),
unread: 0,
last_message: nil,
- updated_at: Utils.to_masto_date(chat.updated_at)
+ updated_at: Utils.to_masto_date(chat.updated_at),
+ pinned: false
}
{:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello")
diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs
index a7af3e74e..cbe200738 100644
--- a/test/pleroma/web/plugs/frontend_static_plug_test.exs
+++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs
@@ -97,6 +97,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
"users",
"tags",
"mailer",
+ "frontend_switcher",
"inbox",
"relay",
"internal",
@@ -113,4 +114,36 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
assert expected_routes == Pleroma.Web.Router.get_api_routes()
end
+
+ describe "preferred frontend cookie handling" do
+ test "returns preferred frontend file", %{conn: conn} do
+ name = "test-fe"
+ ref = "develop"
+
+ clear_config([:frontends, :pickable], ["#{name}/#{ref}"])
+ path = "#{@dir}/frontends/#{name}/#{ref}"
+
+ Pleroma.Backports.mkdir_p!(path)
+ File.write!("#{path}/index.html", "from frontend plug")
+
+ index =
+ conn
+ |> put_req_cookie("preferred_frontend", "#{name}/#{ref}")
+ |> get("/")
+
+ assert html_response(index, 200) == "from frontend plug"
+ end
+
+ test "only returns content from pickable frontends", %{conn: conn} do
+ clear_config([:instance, :static_dir], "instance/static")
+ clear_config([:frontends, :pickable], ["pleroma-fe/develop", "pl-fe/develop"])
+
+ config_file =
+ conn
+ |> put_req_cookie("preferred_frontend", "../../../config")
+ |> get("/config.exs")
+
+ refute response(config_file, 200) =~ "import Config"
+ end
+ end
end
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 85978e824..096ca2d2a 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -883,7 +883,7 @@ defmodule Pleroma.Web.StreamerTest do
assert Streamer.filtered_by_user?(user1, notif)
end
- test "it send non-reblog notification for reblog-muted actors", %{
+ test "it sends non-reblog notification for reblog-muted actors", %{
user: user1,
token: user1_token
} do
diff --git a/tools/check-changelog b/tools/check-changelog
index d053ed577..5952aefcc 100644
--- a/tools/check-changelog
+++ b/tools/check-changelog
@@ -1,5 +1,8 @@
#!/bin/sh
+echo "adding ownership exception"
+git config --global --add safe.directory $(pwd)
+
echo "looking for change log"
git remote add upstream https://git.pleroma.social/pleroma/pleroma.git