Merge remote-tracking branch 'origin/develop' into mastodon-quote-id-api

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk 2025-12-17 13:43:45 +01:00
commit e0ab2c9c9c
129 changed files with 2128 additions and 362 deletions

View file

@ -16,9 +16,15 @@ workflow:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_BRANCH == "stable" - if: $CI_COMMIT_BRANCH == "stable"
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_COMMIT_TAG
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never when: never
# Default artifacts configuration
.default_artifacts: &default_artifacts
expire_in: 30 days
cache: &global_cache_policy cache: &global_cache_policy
key: $CI_JOB_IMAGE-$CI_COMMIT_SHORT_SHA key: $CI_JOB_IMAGE-$CI_COMMIT_SHORT_SHA
paths: paths:
@ -56,6 +62,7 @@ check-changelog:
before_script: '' before_script: ''
after_script: '' after_script: ''
cache: {} cache: {}
artifacts: *default_artifacts
script: script:
- apk add git - apk add git
- sh ./tools/check-changelog - sh ./tools/check-changelog
@ -71,6 +78,7 @@ check-changelog:
.using-ci-base: .using-ci-base:
tags: tags:
- amd64 - amd64
artifacts: *default_artifacts
build-1.15.8-otp-26: build-1.15.8-otp-26:
extends: extends:
@ -101,8 +109,12 @@ spec-build:
artifacts: artifacts:
paths: paths:
- spec.json - spec.json
reports:
dotenv: build.env
expire_in: 42 years
script: script:
- mix pleroma.openapi_spec spec.json - mix pleroma.openapi_spec spec.json
- echo "SPEC_BUILD_JOB_ID=$CI_JOB_ID" >> build.env
benchmark: benchmark:
extends: 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" - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --cover --preload-modules"
coverage: '/^Line total: ([^ ]*%)$/' coverage: '/^Line total: ([^ ]*%)$/'
artifacts: artifacts:
expire_in: 30 days
reports: reports:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
@ -171,6 +184,7 @@ unit-testing-1.18.3-otp-27:
formatting-1.15: formatting-1.15:
extends: .build_changes_policy extends: .build_changes_policy
artifacts: *default_artifacts
image: &formatting_elixir elixir:1.15-alpine image: &formatting_elixir elixir:1.15-alpine
stage: lint stage: lint
cache: *testing_cache_policy cache: *testing_cache_policy
@ -185,6 +199,7 @@ formatting-1.15:
cycles-1.15: cycles-1.15:
extends: .build_changes_policy extends: .build_changes_policy
artifacts: *default_artifacts
image: *formatting_elixir image: *formatting_elixir
stage: lint stage: lint
cache: {} cache: {}
@ -208,7 +223,7 @@ dialyzer:
- .using-ci-base - .using-ci-base
stage: lint stage: lint
allow_failure: true allow_failure: true
when: manual when: manual
cache: *testing_cache_policy cache: *testing_cache_policy
tags: tags:
- feld - feld
@ -217,15 +232,13 @@ dialyzer:
docs-deploy: docs-deploy:
stage: deploy stage: deploy
cache: *testing_cache_policy trigger:
image: alpine:latest project: pleroma/docs
branch: master
strategy: depend
only: only:
- stable@pleroma/pleroma - stable@pleroma/pleroma
- develop@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: review_app:
image: alpine:3.9 image: alpine:3.9
stage: deploy stage: deploy
@ -241,6 +254,7 @@ review_app:
except: except:
- master - master
- develop - develop
artifacts: *default_artifacts
script: script:
- echo "$CI_ENVIRONMENT_SLUG" - echo "$CI_ENVIRONMENT_SLUG"
- mkdir -p ~/.ssh - mkdir -p ~/.ssh
@ -257,21 +271,19 @@ review_app:
spec-deploy: spec-deploy:
stage: deploy stage: deploy
artifacts: trigger:
paths: project: pleroma/api-docs
- spec.json branch: master
strategy: depend
only: only:
- develop@pleroma/pleroma - develop@pleroma/pleroma
image: alpine:latest variables:
before_script: SPEC_BUILD_JOB_ID: $SPEC_BUILD_JOB_ID
- 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
stop_review_app: stop_review_app:
image: alpine:3.9 image: alpine:3.9
stage: deploy stage: deploy
artifacts: *default_artifacts
before_script: before_script:
- apk update && apk add openssh-client git - apk update && apk add openssh-client git
when: manual when: manual

View file

@ -0,0 +1 @@
Fix fetching public keys with authorized fetch enabled

View file

@ -0,0 +1 @@
Use separate schemas for muted/blocked accounts lists

View file

@ -0,0 +1 @@
Fix CI changelog checker

View file

View file

@ -0,0 +1 @@
Use :list_behaviour_implementations for LanguageDetector and Translation providers

View file

@ -0,0 +1 @@
Support new Mastodon API for endorsed accounts

View file

@ -0,0 +1 @@
Allow FediIndex crawler bot by default

View file

@ -0,0 +1 @@
Allow filtering users with `accepts_chat_messages` capability

View file

@ -0,0 +1 @@
Add `timelines_access` to InstanceView

View file

@ -0,0 +1 @@
Use end-of-string in regex for local `get_by_nickname`

View file

@ -0,0 +1 @@
Respect restrict_unauthenticated in /api/v1/accounts/lookup

View file

@ -0,0 +1 @@
Use Mastodon-compatible route for quotes list and param for quotes count

View file

@ -0,0 +1 @@
MRF InlineQuotePolicy: Don't inline quoted post URL in Mastodon quote posts

View file

@ -0,0 +1 @@
Fix NodeInfo content-type

View file

@ -0,0 +1 @@
Add Actor images normalization from array of urls to string

View file

View file

@ -0,0 +1 @@
remove duplicated code from notificationview

View file

@ -0,0 +1 @@
Order favourites and reblogs list from newest to oldest

View file

@ -0,0 +1 @@
Add /api/v1/pleroma/outgoing_follow_requests

View file

@ -0,0 +1 @@
Allow to pin/unpip chats

1
changelog.d/plaroma.skip Normal file
View file

@ -0,0 +1 @@
i don't think it's called plaroma

View file

@ -0,0 +1 @@
Allow users to select preferred frontend

View file

@ -0,0 +1 @@
`remote_url` links to unproxied URL

View file

@ -0,0 +1 @@
Allow setting custom user-agent for fetching rich media content

View file

@ -0,0 +1 @@
Redirect /users/:nickname.rss to /users/:nickname/feed.rss instead of .atom

View file

@ -0,0 +1 @@
Add `write:scrobbles` and `read:scrobbles` scope for scrobbling

View file

@ -0,0 +1 @@
Scrubber: Allow `quote-inline` class in <p> tags used by Mastodon quotes

View file

@ -0,0 +1 @@
Allow "invisible" and "ellipsis" classes for span tags to match Mastodon behavior

View file

@ -0,0 +1 @@
Send push notifications for statuses from subscribed accounts

View file

@ -0,0 +1 @@
Stream marker updates

View file

@ -0,0 +1 @@
Support Mozhi translation provider

View file

@ -0,0 +1 @@
Support translateLocally translation provider

View file

@ -0,0 +1 @@
Fix sometimes incorrect URI percent encoding

View file

@ -2131,6 +2131,11 @@ config :pleroma, :config_description, [
description: description:
"Amount of milliseconds after which the HTTP request is forcibly terminated.", "Amount of milliseconds after which the HTTP request is forcibly terminated.",
suggestions: [5_000] 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: description:
"A map containing available frontends and parameters for their installation.", "A map containing available frontends and parameters for their installation.",
children: frontend_options 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, key: :provider,
type: :module, type: :module,
suggestions: [ suggestions: {:list_behaviour_implementations, Pleroma.Language.LanguageDetector.Provider}
Pleroma.Language.LanguageDetector.Fasttext
]
}, },
%{ %{
group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext}, group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext},
@ -3556,10 +3565,7 @@ config :pleroma, :config_description, [
%{ %{
key: :provider, key: :provider,
type: :module, type: :module,
suggestions: [ suggestions: {:list_behaviour_implementations, Pleroma.Language.Translation.Provider}
Pleroma.Language.Translation.Deepl,
Pleroma.Language.Translation.Libretranslate
]
}, },
%{ %{
group: {:subgroup, Pleroma.Language.Translation.Deepl}, group: {:subgroup, Pleroma.Language.Translation.Deepl},
@ -3588,6 +3594,27 @@ config :pleroma, :config_description, [
label: "LibreTranslate API Key", label: "LibreTranslate API Key",
type: :string, type: :string,
suggestions: ["YOUR_API_KEY"] 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"]
} }
] ]
} }

View file

@ -66,9 +66,9 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 2, "unread": 2,
"last_message" : {...}, // The last message in that chat "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
``` ```
@ -93,8 +93,8 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 0, "unread": 0,
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
``` ```
@ -111,7 +111,7 @@ The modified chat message
### Getting a list of Chats ### 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 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). 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: Parameters:
- with_muted: Include chats from muted users (boolean). - with_muted: Include chats from muted users (boolean).
- pinned: Include only pinned chats (boolean).
Returned data: Returned data:
@ -130,16 +131,16 @@ Returned data:
"username": "somenick", "username": "somenick",
... ...
}, },
"id" : "1", "id": "1",
"unread" : 2, "unread": 2,
"last_message" : {...}, // The last message in that chat "last_message": {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z" "updated_at": "2020-04-21T15:11:46.000Z"
} }
] ]
``` ```
The recipient of messages that are sent to this chat is given by their AP ID. 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 ### 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. 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 ### 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`: 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`:

View file

@ -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. - `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. - `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. - `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). - `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). - `list_id`: the ID of the list the post is addressed to (if any, only returned to author).

View file

@ -684,6 +684,7 @@ Audio scrobbling in Pleroma is **deprecated**.
### Creates a new Listen activity for an account ### Creates a new Listen activity for an account
* Method `POST` * Method `POST`
* Authentication: required * Authentication: required
* OAuth scope: `write:scrobbles`
* Params: * Params:
* `title`: the title of the media playing * `title`: the title of the media playing
* `album`: the album of the media playing [optional] * `album`: the album of the media playing [optional]

View file

@ -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: 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`) * Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
## `ffmpeg` ## `ffmpeg`
@ -33,5 +33,5 @@ It is required for the following Pleroma features:
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.StripLocation` upload filter (related config: `Pleroma.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.ReadDescription` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)

View file

@ -25,6 +25,8 @@ defmodule Pleroma.Chat do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:recipient, :string) field(:recipient, :string)
field(:pinned, :boolean)
timestamps() timestamps()
end end
@ -94,4 +96,16 @@ defmodule Pleroma.Chat do
order_by: [desc: c.updated_at] order_by: [desc: c.updated_at]
) )
end 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 end

View file

@ -132,6 +132,13 @@ defmodule Pleroma.Constants do
do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/ 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(upload_object_types, do: ["Document", "Image"])
const(activity_json_canonical_mime_type, const(activity_json_canonical_mime_type,

View file

@ -157,6 +157,16 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.all() |> Repo.all()
end 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 def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__ __MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept) |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)

View file

@ -131,31 +131,4 @@ defmodule Pleroma.HTTP do
defp default_middleware, defp default_middleware,
do: [Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.EncodeUrl] 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 end

View file

@ -16,7 +16,12 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
config_opts = Pleroma.Config.get([:http, :adapter], []) config_opts = Pleroma.Config.get([:http, :adapter], [])
url_encoding =
Keyword.new()
|> Keyword.put(:path_encode_fun, fn path -> path end)
@defaults @defaults
|> Keyword.merge(url_encoding)
|> Keyword.merge(config_opts) |> Keyword.merge(config_opts)
|> Keyword.merge(connection_opts) |> Keyword.merge(connection_opts)
|> add_scheme_opts(uri) |> add_scheme_opts(uri)

View file

@ -0,0 +1,109 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,129 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -282,10 +282,15 @@ defmodule Pleroma.Notification do
select: n.id select: n.id
) )
Multi.new() {:ok, %{marker: marker}} =
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()]) Multi.new()
|> Marker.multi_set_last_read_id(user, "notifications") |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|> Repo.transaction() |> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
Streamer.stream(["user", "user:notification"], marker)
{:ok, %{marker: marker}}
end end
@spec read_one(User.t(), String.t()) :: @spec read_one(User.t(), String.t()) ::
@ -526,9 +531,7 @@ defmodule Pleroma.Notification do
%Activity{data: %{"type" => "Create"}} = activity, %Activity{data: %{"type" => "Create"}} = activity,
local_only local_only
) do ) do
notification_enabled_ap_ids = notification_enabled_ap_ids = Utils.get_notified_subscribers(activity)
[]
|> Utils.maybe_notify_subscribers(activity)
potential_receivers = potential_receivers =
User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only) User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do defmodule Pleroma.ReverseProxy do
alias Pleroma.Utils.URIEncoding
@range_headers ~w(range if-range) @range_headers ~w(range if-range)
@keep_req_headers ~w(accept accept-encoding cache-control if-modified-since) ++ @keep_req_headers ~w(accept accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match) ++ @range_headers ~w(if-unmodified-since if-none-match) ++ @range_headers
@ -155,11 +157,12 @@ defmodule Pleroma.ReverseProxy do
end end
defp request(method, url, headers, opts) do defp request(method, url, headers, opts) do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom() method = method |> String.downcase() |> String.to_existing_atom()
url = maybe_encode_url(url) url = maybe_encode_url(url)
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
case client().request(method, url, headers, "", opts) do case client().request(method, url, headers, "", opts) do
{:ok, code, headers, client} when code in @valid_resp_codes -> {:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client} {:ok, code, downcase_headers(headers), client}
@ -459,9 +462,9 @@ defmodule Pleroma.ReverseProxy do
# Also do it for test environment # Also do it for test environment
defp maybe_encode_url(url) do defp maybe_encode_url(url) do
case Application.get_env(:tesla, :adapter) do case Application.get_env(:tesla, :adapter) do
Tesla.Adapter.Hackney -> Pleroma.HTTP.encode_url(url) Tesla.Adapter.Hackney -> URIEncoding.encode_url(url)
{Tesla.Adapter.Finch, _} -> Pleroma.HTTP.encode_url(url) {Tesla.Adapter.Finch, _} -> URIEncoding.encode_url(url)
Tesla.Mock -> Pleroma.HTTP.encode_url(url) Tesla.Mock -> URIEncoding.encode_url(url)
_ -> url _ -> url
end end
end end

View file

@ -7,6 +7,11 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true @impl true
def request(method, url, headers, body, opts \\ []) do 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) :hackney.request(method, url, headers, body, opts)
end end

View file

@ -54,7 +54,7 @@ defmodule Pleroma.Signature do
def fetch_public_key(conn) do def fetch_public_key(conn) do
with {:ok, actor_id} <- get_actor_id(conn), 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} {:ok, public_key}
else else
e -> e ->

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Tesla.Middleware.EncodeUrl do
@impl Tesla.Middleware @impl Tesla.Middleware
def call(%Tesla.Env{url: url} = env, next, _) do 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} env = %{env | url: url}

View file

@ -35,6 +35,7 @@ defmodule Pleroma.Upload do
""" """
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.Utils.URIEncoding
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
require Logger require Logger
@ -230,11 +231,18 @@ defmodule Pleroma.Upload do
tmp_path tmp_path
end end
# Encoding the whole path here is fine since the path is in a
# UUID/<file name> 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 defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
encode_opts = [bypass_decode: true, bypass_parse: true]
path = path =
URI.encode(path, &char_unescaped?/1) <> URIEncoding.encode_url(path, encode_opts) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}" enum = %{name: name}
"?#{URI.encode_query(enum)}"
else else
"" ""
end end

View file

@ -233,8 +233,8 @@ defmodule Pleroma.User do
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do @user_relationships_config do
# `def blocked_users_relation/2`, `def muted_users_relation/2`, # `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`, # `def reblog_muted_users_relation/2`, `def notification_muted_users_relation/2`,
# `def subscriber_users/2`, `def endorsed_users_relation/2` # `def subscriber_users_relation/2`, `def endorsed_users_relation/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target)) 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?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(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 defdelegate search(query, opts \\ []), to: User.Search
@doc """ @doc """
@ -1357,7 +1358,7 @@ defmodule Pleroma.User do
@spec get_by_nickname(String.t()) :: User.t() | nil @spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) || 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)) Repo.get_by(User, nickname: local_nickname(nickname))
end end
end end
@ -2307,6 +2308,15 @@ defmodule Pleroma.User do
def public_key(_), do: {:error, "key not found"} 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 def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_cached_by_ap_id(ap_id), with %User{} = user <- get_cached_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do {:ok, public_key} <- public_key(user) do

View file

@ -16,6 +16,7 @@ defmodule Pleroma.User.Search do
following = Keyword.get(opts, :following, false) following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit) result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0) offset = Keyword.get(opts, :offset, 0)
capabilities = Keyword.get(opts, :capabilities, [])
for_user = Keyword.get(opts, :for_user) for_user = Keyword.get(opts, :for_user)
@ -32,7 +33,7 @@ defmodule Pleroma.User.Search do
results = results =
query_string 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) |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
results results
@ -80,7 +81,7 @@ defmodule Pleroma.User.Search do
end end
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 for_user
|> base_query(following) |> base_query(following)
|> filter_blocked_user(for_user) |> filter_blocked_user(for_user)
@ -94,6 +95,7 @@ defmodule Pleroma.User.Search do
|> subquery() |> subquery()
|> order_by(desc: :search_rank) |> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user) |> maybe_restrict_local(for_user)
|> maybe_restrict_accepting_chat_messages(capabilities)
|> filter_deactivated_users() |> filter_deactivated_users()
end end
@ -214,6 +216,14 @@ defmodule Pleroma.User.Search do
end end
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 limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true) defp restrict_local(q), do: where(q, [u], u.local == true)

View file

@ -0,0 +1,142 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2025 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -1569,7 +1569,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_actor_url(_url), do: nil 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", "type" => "Image",
"url" => [%{"href" => url}] "url" => [%{"href" => url}]
@ -1577,6 +1577,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_put_description(data) |> maybe_put_description(data)
end 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(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil defp normalize_image(_), do: nil

View file

@ -18,6 +18,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
content =~ quote_url -> true content =~ quote_url -> true
# Does the content already have a .quote-inline span? # Does the content already have a .quote-inline span?
content =~ "<span class=\"quote-inline\">" -> true content =~ "<span class=\"quote-inline\">" -> true
# Does the content already have a .quote-inline p? (Mastodon)
content =~ "<p class=\"quote-inline\">" -> true
# No inline quote found # No inline quote found
true -> false true -> false
end end

View file

@ -151,7 +151,8 @@ defmodule Pleroma.Web.ApiSpec do
"Suggestions", "Suggestions",
"Announcements", "Announcements",
"Remote interaction", "Remote interaction",
"Others" "Others",
"Preferred frontends"
] ]
} }
] ]

View file

@ -398,6 +398,28 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
} }
end 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 def remove_from_followers_operation do
%Operation{ %Operation{
tags: ["Account actions"], tags: ["Account actions"],
@ -461,7 +483,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["follow", "read:mutes"]}], security: [%{"oAuth" => ["follow", "read:mutes"]}],
parameters: [with_relationships_param() | pagination_params()], parameters: [with_relationships_param() | pagination_params()],
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_muted_accounts())
} }
} }
end end
@ -475,7 +497,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:blocks"]}], security: [%{"oAuth" => ["read:blocks"]}],
parameters: [with_relationships_param() | pagination_params()], parameters: [with_relationships_param() | pagination_params()],
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_blocked_accounts())
} }
} }
end end
@ -495,16 +517,17 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
], ],
responses: %{ responses: %{
200 => Operation.response("Account", "application/json", Account), 200 => Operation.response("Account", "application/json", Account),
401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError) 404 => Operation.response("Error", "application/json", ApiError)
} }
} }
end end
def endorsements_operation do def own_endorsements_operation do
%Operation{ %Operation{
tags: ["Retrieve account information"], tags: ["Retrieve account information"],
summary: "Endorsements", summary: "Endorsements",
operationId: "AccountController.endorsements", operationId: "AccountController.own_endorsements",
description: "Returns endorsed accounts", description: "Returns endorsed accounts",
security: [%{"oAuth" => ["read:accounts"]}], security: [%{"oAuth" => ["read:accounts"]}],
responses: %{ responses: %{
@ -874,6 +897,54 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
} }
end 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 defp array_of_relationships do
%Schema{ %Schema{
title: "ArrayOfRelationships", title: "ArrayOfRelationships",

View file

@ -142,7 +142,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query, :query,
BooleanLike.schema(), BooleanLike.schema(),
"Include chats from muted users" "Include chats from muted users"
) ),
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
], ],
responses: %{ responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response()) 200 => Operation.response("The chats of the user", "application/json", chats_response())
@ -166,7 +167,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
:query, :query,
BooleanLike.schema(), BooleanLike.schema(),
"Include chats from muted users" "Include chats from muted users"
) ),
Operation.parameter(:pinned, :query, BooleanLike.schema(), "Include only pinned chats")
| pagination_params() | pagination_params()
], ],
responses: %{ responses: %{
@ -257,6 +259,44 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
} }
end 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 def chats_response do
%Schema{ %Schema{
title: "ChatsResponse", title: "ChatsResponse",

View file

@ -64,25 +64,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
} }
end 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 def subscribe_operation do
%Operation{ %Operation{
deprecated: true, deprecated: true,

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -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

View file

@ -20,7 +20,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
%Operation{ %Operation{
tags: ["Scrobbles"], tags: ["Scrobbles"],
summary: "Creates a new Listen activity for an account", summary: "Creates a new Listen activity for an account",
security: [%{"oAuth" => ["write"]}], security: [%{"oAuth" => ["write:scrobbles"]}],
operationId: "PleromaAPI.ScrobbleController.create", operationId: "PleromaAPI.ScrobbleController.create",
deprecated: true, deprecated: true,
requestBody: request_body("Parameters", create_request(), required: true), requestBody: request_body("Parameters", create_request(), required: true),
@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
parameters: [ parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params() %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
], ],
security: [%{"oAuth" => ["read"]}], security: [%{"oAuth" => ["read:scrobbles"]}],
responses: %{ responses: %{
200 => 200 =>
Operation.response("Array of Scrobble", "application/json", %Schema{ Operation.response("Array of Scrobble", "application/json", %Schema{

View file

@ -19,7 +19,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do
%Operation{ %Operation{
tags: ["Retrieve status information"], tags: ["Retrieve status information"],
summary: "Quoted by", 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", operationId: "PleromaAPI.StatusController.quotes",
parameters: [id_param() | pagination_params()], parameters: [id_param() | pagination_params()],
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],

View file

@ -46,6 +46,12 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
:query, :query,
%Schema{allOf: [BooleanLike], default: false}, %Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following" "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: %{ responses: %{

View file

@ -549,6 +549,27 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
} }
end 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 def array_of_statuses do
%Schema{type: :array, items: Status, example: [Status.schema().example]} %Schema{type: :array, items: Status, example: [Status.schema().example]}
end end

View file

@ -33,8 +33,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
header: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri},
id: FlakeID, id: FlakeID,
locked: %Schema{type: :boolean}, 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}, note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer}, statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri}, url: %Schema{type: :string, format: :uri},

View file

@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
account: %Schema{type: :object}, account: %Schema{type: :object},
unread: %Schema{type: :integer}, unread: %Schema{type: :integer},
last_message: ChatMessage, last_message: ChatMessage,
updated_at: %Schema{type: :string, format: :"date-time"} updated_at: %Schema{type: :string, format: :"date-time"},
pinned: %Schema{type: :boolean}
}, },
example: %{ example: %{
"account" => %{ "account" => %{
@ -69,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
"id" => "1", "id" => "1",
"unread" => 2, "unread" => 2,
"last_message" => ChatMessage.schema().example, "last_message" => ChatMessage.schema().example,
"updated_at" => "2020-04-21T15:06:45.000Z" "updated_at" => "2020-04-21T15:06:45.000Z",
"pinned" => false
} }
}) })
end end

View file

@ -219,7 +219,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}, },
quotes_count: %Schema{ quotes_count: %Schema{
type: :integer, 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{ local: %Schema{
type: :boolean, 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"}, 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{ reblog: %Schema{
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}], allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
nullable: true, nullable: true,
@ -385,6 +391,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"quotes_count" => 0 "quotes_count" => 0
}, },
"poll" => nil, "poll" => nil,
"quotes_count" => 0,
"reblog" => nil, "reblog" => nil,
"reblogged" => false, "reblogged" => false,
"reblogs_count" => 0, "reblogs_count" => 0,

View file

@ -402,28 +402,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_mentioned_recipients(recipients, _), do: recipients 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 def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
user user
@ -437,6 +415,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_notify_followers(recipients, _), do: recipients 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 def maybe_extract_mentions(%{"tag" => tag}) do
tag tag
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end) |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)

View file

@ -30,7 +30,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end end
def redirector(conn, _params, code \\ 200) do 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 = response =
index_content index_content
@ -51,7 +51,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end end
def redirector_with_meta(conn, params) do 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) tags = build_tags(conn, params)
preloads = preload_data(conn, params) preloads = preload_data(conn, params)
@ -69,7 +69,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do
end end
def redirector_with_preload(conn, params) do 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) preloads = preload_data(conn, params)
response = response =
@ -91,8 +91,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do
|> text("") |> text("")
end end
defp index_file_path do defp index_file_path(conn) do
Pleroma.Web.Plugs.InstanceStatic.file_path("index.html") frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary)
Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type)
end end
defp build_tags(conn, params) do defp build_tags(conn, params) do

View file

@ -28,9 +28,12 @@ defmodule Pleroma.Web.Feed.UserController do
ActivityPubController.call(conn, :user) ActivityPubController.call(conn, :user)
end 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 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
end end

View file

@ -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(<meta http-equiv="refresh" content="0; url=/">))
end
end

View file

@ -0,0 +1,5 @@
defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
end

View file

@ -31,14 +31,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false) 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(:skip_public_check when action in [:show, :statuses])
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:show, :followers, :following] when action in [:show, :followers, :following, :lookup, :endorsements]
) )
plug( plug(
@ -50,7 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"]} %{scopes: ["read:accounts"]}
when action in [:verify_credentials, :endorsements] when action in [:verify_credentials, :endorsements, :own_endorsements]
) )
plug( plug(
@ -89,7 +89,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@relationship_actions [:follow, :unfollow, :remove_from_followers] @relationship_actions [:follow, :unfollow, :remove_from_followers]
@needs_account ~W( @needs_account ~W(
followers following lists follow unfollow mute unmute block unblock followers following lists follow unfollow mute unmute block unblock
note endorse unendorse remove_from_followers note endorse unendorse endorsements remove_from_followers
)a )a
plug( plug(
@ -555,6 +555,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
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" @doc "POST /api/v1/accounts/:id/remove_from_followers"
def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"} {:error, "Can not unfollow yourself"}
@ -619,8 +635,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
@doc "GET /api/v1/accounts/lookup" @doc "GET /api/v1/accounts/lookup"
def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do def lookup(
with %User{} = user <- User.get_by_nickname(nickname) do %{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", render(conn, "show.json",
user: user, user: user,
skip_visibility_check: true skip_visibility_check: true
@ -631,7 +652,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
@doc "GET /api/v1/endorsements" @doc "GET /api/v1/endorsements"
def endorsements(%{assigns: %{user: user}} = conn, params) do def own_endorsements(%{assigns: %{user: user}} = conn, params) do
users = users =
user user
|> User.endorsed_users_relation(_restrict_deactivated = true) |> User.endorsed_users_relation(_restrict_deactivated = true)

View file

@ -91,6 +91,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
limit: min(params[:limit], @search_limit), limit: min(params[:limit], @search_limit),
offset: params[:offset], offset: params[:offset],
type: params[:type], type: params[:type],
capabilities: params[:capabilities],
author: get_author(params), author: get_author(params),
embed_relationships: ControllerHelper.embed_relationships?(params), embed_relationships: ControllerHelper.embed_relationships?(params),
for_user: user for_user: user

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
only: [try_render: 3, add_link_headers: 2] only: [try_render: 3, add_link_headers: 2]
require Ecto.Query require Ecto.Query
require Pleroma.Constants
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark alias Pleroma.Bookmark
@ -41,7 +42,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:show, :show,
:context, :context,
:show_history, :show_history,
:show_source :show_source,
:quotes
] ]
) )
@ -488,6 +490,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
users = users =
User User
|> Ecto.Query.where([u], u.ap_id in ^likes) |> Ecto.Query.where([u], u.ap_id in ^likes)
|> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^likes, u.ap_id))
|> Repo.all() |> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1))) |> Enum.filter(&(not User.blocks?(user, &1)))
@ -523,6 +526,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
users = users =
User User
|> Ecto.Query.where([u], u.ap_id in ^announces) |> Ecto.Query.where([u], u.ap_id in ^announces)
|> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^announces, u.ap_id))
|> Repo.all() |> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1))) |> Enum.filter(&(not User.blocks?(user, &1)))
@ -629,6 +633,45 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
) )
end 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 defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app %{client_name: client_name, website: website} = Repo.preload(token, :app).app

View file

@ -146,6 +146,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"pleroma_emoji_reactions", "pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions", "pleroma_custom_emoji_reactions",
"pleroma_chat_messages", "pleroma_chat_messages",
"pleroma:pin_chats",
if Config.get([:instance, :show_reactions]) do if Config.get([:instance, :show_reactions]) do
"exposable_reactions" "exposable_reactions"
end, end,
@ -257,10 +258,34 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
vapid: %{ vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) 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 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 defp pleroma_configuration(instance) do
base_urls = %{} base_urls = %{}

View file

@ -106,27 +106,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
} }
case notification.type do case notification.type do
"mention" -> type when type in ["mention", "status", "poll"] ->
put_status(response, activity, reading_user, status_render_opts) put_status(response, activity, reading_user, status_render_opts)
"status" -> type when type in ["favourite", "reblog", "update"] ->
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" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts) put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" -> "move" ->
put_target(response, activity, reading_user, %{}) put_target(response, activity, reading_user, %{})
"poll" ->
put_status(response, activity, reading_user, status_render_opts)
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
response response
|> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_status(parent_activity_fn.(), reading_user, status_render_opts)

View file

@ -447,6 +447,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
application: build_application(object.data["generator"]), application: build_application(object.data["generator"]),
language: get_language(object), language: get_language(object),
emojis: build_emojis(object.data["emoji"]), emojis: build_emojis(object.data["emoji"]),
quotes_count: object.data["quotesCount"] || 0,
pleroma: %{ pleroma: %{
local: activity.local, local: activity.local,
conversation_id: get_context_id(activity), conversation_id: get_context_id(activity),
@ -602,7 +603,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"] [attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" 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() href_preview = attachment_url["href"] |> MediaProxy.preview_url()
meta = render("attachment_meta.json", %{attachment: attachment}) meta = render("attachment_meta.json", %{attachment: attachment})
@ -641,7 +643,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
%{ %{
id: attachment_id, id: attachment_id,
url: href, url: href,
remote_url: href, remote_url: href_remote,
preview_url: href_preview, preview_url: href_preview,
text_url: href, text_url: href,
type: type, type: type,

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Helpers.UriHelper alias Pleroma.Helpers.UriHelper
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.Utils.URIEncoding
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy.Invalidation alias Pleroma.Web.MediaProxy.Invalidation
@ -99,13 +100,21 @@ defmodule Pleroma.Web.MediaProxy do
{base64, sig64} {base64, sig64}
end 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 def encode_url(url) do
url = URIEncoding.encode_url(url)
{base64, sig64} = base64_sig64(url) {base64, sig64} = base64_sig64(url)
build_url(sig64, base64, filename(url)) build_url(sig64, base64, filename(url))
end end
def encode_preview_url(url, preview_params \\ []) do def encode_preview_url(url, preview_params \\ []) do
url = URIEncoding.encode_url(url)
{base64, sig64} = base64_sig64(url) {base64, sig64} = base64_sig64(url)
build_preview_url(sig64, base64, filename(url), preview_params) build_preview_url(sig64, base64, filename(url), preview_params)

View file

@ -36,7 +36,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
conn conn
|> put_resp_header( |> put_resp_header(
"content-type", "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) |> json(node_info)
end end

View file

@ -9,7 +9,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
only: [ only: [
json_response: 3, json_response: 3,
add_link_headers: 2, add_link_headers: 2,
embed_relationships?: 1,
assign_account_by_id: 2 assign_account_by_id: 2
] ]
@ -45,12 +44,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
) )
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action == :endorsements
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"]} when action == :birthdays %{scopes: ["read:accounts"]} when action == :birthdays
@ -60,7 +53,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug( plug(
:assign_account_by_id :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 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
@ -109,22 +102,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
) )
end 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" @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, _subscription} <- User.subscribe(user, subscription_target) do with {:ok, _subscription} <- User.subscribe(user, subscription_target) do

View file

@ -29,7 +29,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
:create, :create,
:mark_as_read, :mark_as_read,
:mark_message_as_read, :mark_message_as_read,
:delete_message :delete_message,
:pin,
:unpin
] ]
) )
@ -199,8 +201,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
user_id user_id
|> Chat.for_user_query() |> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users) |> where([c], c.recipient not in ^exclude_users)
|> restrict_pinned(params)
end 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 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), with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
@ -214,6 +224,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end end
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 defp idempotency_key(conn) do
case get_req_header(conn, "idempotency-key") do case get_req_header(conn, "idempotency-key") do
[key] -> key [key] -> key

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -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

View file

@ -16,10 +16,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
plug( plug(
OAuthScopesPlug, 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 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation

View file

@ -5,16 +5,9 @@
defmodule Pleroma.Web.PleromaAPI.StatusController do defmodule Pleroma.Web.PleromaAPI.StatusController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
require Ecto.Query require Ecto.Query
require Pleroma.Constants 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 alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate) 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 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaStatusOperation
@doc "GET /api/v1/pleroma/statuses/:id/quotes" @doc "GET /api/v1/pleroma/statuses/:id/quotes"
def quotes(%{assigns: %{user: user}} = conn, %{id: id} = params) do def quotes(conn, _params) do
with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id), conn
true <- Visibility.visible_for_user?(activity, user) do |> put_view(Pleroma.Web.MastodonAPI.StatusView)
params = |> Pleroma.Web.MastodonAPI.StatusController.call(:quotes)
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
end end
end end

View file

@ -24,7 +24,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
last_message: last_message:
last_message && last_message &&
MessageReferenceView.render("show.json", chat_message_reference: 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 end

View file

@ -5,17 +5,23 @@
defmodule Pleroma.Web.Plugs.FrontendStatic do defmodule Pleroma.Web.Plugs.FrontendStatic do
require Pleroma.Constants require Pleroma.Constants
@frontend_cookie_name "preferred_frontend"
@moduledoc """ @moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
""" """
@behaviour Plug @behaviour Plug
def file_path(path, frontend_type \\ :primary) do defp instance_static_path do
if configuration = Pleroma.Config.get([:frontends, frontend_type]) do Pleroma.Config.get([:instance, :static_dir], "instance/static")
instance_static_path = 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([ Path.join([
instance_static_path, instance_static_path(),
"frontends", "frontends",
configuration["name"], configuration["name"],
configuration["ref"], configuration["ref"],
@ -26,6 +32,15 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
end end
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 def init(opts) do
opts opts
|> Keyword.put(:from, "__unconfigured_frontend_static_plug") |> Keyword.put(:from, "__unconfigured_frontend_static_plug")
@ -36,7 +51,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
def call(conn, opts) do def call(conn, opts) do
with false <- api_route?(conn.path_info), with false <- api_route?(conn.path_info),
false <- invalid_path?(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 path when not is_nil(path) <- file_path("", frontend_type) do
call_static(conn, opts, path) call_static(conn, opts, path)
else else
@ -45,6 +61,31 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do
end end
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 defp invalid_path?(list) do
invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"])) invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
end end

View file

@ -13,11 +13,11 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
""" """
@behaviour Plug @behaviour Plug
def file_path(path) do def file_path(path, frontend_type \\ :primary) do
instance_path = instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), 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) || (File.exists?(instance_path) && instance_path) ||
(frontend_path && File.exists?(frontend_path) && frontend_path) || (frontend_path && File.exists?(frontend_path) && frontend_path) ||

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do
end end
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength # 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 defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types) alerts = Map.take(alerts, @supported_alert_types)

View file

@ -126,6 +126,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end end
defp req_headers do 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
end end

View file

@ -561,6 +561,18 @@ defmodule Pleroma.Web.Router do
get("/apps", AppController, :index) get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index) get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index) get("/statuses/:id/reactions", EmojiReactionController, :index)
get(
"/preferred_frontend/available",
FrontendSettingsController,
:available_frontends
)
put(
"/preferred_frontend",
FrontendSettingsController,
:update_preferred_frontend
)
end end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do 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) delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read) post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_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/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show) get("/conversations/:id", ConversationController, :show)
@ -603,12 +617,13 @@ defmodule Pleroma.Web.Router do
post("/bookmark_folders", BookmarkFolderController, :create) post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update) patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete) delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
get("/outgoing_follow_requests", FollowRequestController, :outgoing)
end end
scope [] do scope [] do
pipe_through(:api) pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites) get("/accounts/:id/favourites", AccountController, :favourites)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/statuses/:id/quotes", StatusController, :quotes) get("/statuses/:id/quotes", StatusController, :quotes)
end end
@ -637,6 +652,11 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/scrobbles", ScrobbleController, :index) get("/accounts/:id/scrobbles", ScrobbleController, :index)
end 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 "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do scope [] do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
@ -653,7 +673,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/relationships", AccountController, :relationships) get("/accounts/relationships", AccountController, :relationships)
get("/accounts/familiar_followers", AccountController, :familiar_followers) get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists) get("/accounts/:id/lists", AccountController, :lists)
get("/endorsements", AccountController, :endorsements) get("/endorsements", AccountController, :own_endorsements)
get("/blocks", AccountController, :blocks) get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes) get("/mutes", AccountController, :mutes)
@ -667,6 +687,8 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/note", AccountController, :note) post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse) post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse) 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) post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index) get("/conversations", ConversationController, :index)
@ -742,6 +764,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", StatusController, :mute_conversation) post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/statuses/:id/translate", StatusController, :translate) post("/statuses/:id/translate", StatusController, :translate)
get("/statuses/:id/quotes", StatusController, :quotes)
post("/push/subscription", SubscriptionController, :create) post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show) get("/push/subscription", SubscriptionController, :show)
@ -782,6 +805,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/statuses", AccountController, :statuses) get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers) get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following) get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/accounts/:id", AccountController, :show) get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create) post("/accounts", AccountController, :create)
@ -894,7 +918,11 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
pipe_through(:browser) pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
get("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :switch)
post("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :do_switch)
end end
pipeline :ap_service_actor do pipeline :ap_service_actor do

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Chat.MessageReference alias Pleroma.Chat.MessageReference
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Marker
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@ -321,6 +322,16 @@ defmodule Pleroma.Web.Streamer do
end) end)
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 defp do_stream(topic, item) do
Logger.debug("Trying to push to #{topic}") Logger.debug("Trying to push to #{topic}")
Logger.debug("Pushing item to #{topic}") Logger.debug("Pushing item to #{topic}")

View file

@ -0,0 +1,7 @@
<h2>Switch frontend</h2>
<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %>
<%= select(f, :frontend, @choices) %>
<%= submit do: "submit" %>
<% end %>

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.StreamerView do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Marker
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
@ -164,6 +165,19 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!() |> Jason.encode!()
end 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 def render("pleroma_respond.json", %{type: type, result: result} = params) do
%{ %{
event: "pleroma:respond", event: "pleroma:respond",

View file

@ -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

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddAcceptsChatMessagesIndexToUsers do
use Ecto.Migration
def change do
create(index(:users, [:accepts_chat_messages]))
end
end

Some files were not shown because too many files have changed in this diff Show more