Merge remote-tracking branch 'origin/develop' into instance-v2
This commit is contained in:
commit
1e9333a9aa
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
|
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
|
||||||
|
|
||||||
`<type>` can be `add`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the MR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
|
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the MR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
|
||||||
|
|
||||||
In the file, write the changelog entry. For example, if an MR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
|
In the file, write the changelog entry. For example, if an MR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
### Release checklist
|
### Release checklist
|
||||||
* [ ] Bump version in `mix.exs`
|
* [ ] Bump version in `mix.exs`
|
||||||
* [ ] Compile a changelog
|
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||||
* [ ] Create an MR with an announcement to pleroma.social
|
* [ ] Create an MR with an announcement to pleroma.social
|
||||||
#### post-merge
|
#### post-merge
|
||||||
* [ ] Tag the release on the merge commit
|
* [ ] Tag the release on the merge commit
|
||||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -4,19 +4,65 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## 2.6.0
|
||||||
|
### Security
|
||||||
### Changed
|
- Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes.
|
||||||
|
- CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID
|
||||||
|
- Disable XML entity resolution completely to fix a dos vulnerability
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Support for Image activities, namely from Hubzilla
|
- Support for Image activities, namely from Hubzilla
|
||||||
|
- Add OAuth scope descriptions
|
||||||
|
- Allow lang attribute in status text
|
||||||
|
- OnlyMedia Upload Filter
|
||||||
|
- Implement MRF policy to reject or delist according to emojis
|
||||||
|
- (hardening) Add no_new_privs=yes to OpenRC service files
|
||||||
|
- Implement quotes
|
||||||
|
- Add unified streaming endpoint
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- rel="me" was missing its cache
|
- rel="me" was missing its cache
|
||||||
|
- MediaProxy responses now return a sandbox CSP header
|
||||||
|
- Filter context activities using Visibility.visible_for_user?
|
||||||
|
- UploadedMedia: Add missing disposition_type to Content-Disposition
|
||||||
|
- fix not being able to fetch flash file from remote instance
|
||||||
|
- Fix abnormal behaviour when refetching a poll
|
||||||
|
- Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects"
|
||||||
|
- Fix opengraph and twitter card meta tags
|
||||||
|
- ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts
|
||||||
|
- OEmbed HTML tags are now filtered
|
||||||
|
- Restrict attachments to only uploaded files only
|
||||||
|
- Fix error 404 when deleting status of a banned user
|
||||||
|
- Fix config ownership in dockerfile to pass restriction test
|
||||||
|
- Fix user fetch completely broken if featured collection is not in a supported form
|
||||||
|
- Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty
|
||||||
|
- Fix handling report from a deactivated user
|
||||||
|
- Prevent using the .json format to bypass authorized fetch mode
|
||||||
|
- Fix mentioning punycode domains when using Markdown
|
||||||
|
- Show more informative errors when profile exceeds char limits
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact)
|
- BREAKING: Support for passwords generated with `crypt(3)` (Gnu Social migration artifact)
|
||||||
|
- remove BBS/SSH feature, replaced by an external bridge.
|
||||||
|
- Remove a few unused indexes.
|
||||||
|
- Cleanup OStatus-era user upgrades and ap_enabled indicator
|
||||||
|
- Deprecate Pleroma's audio scrobbling
|
||||||
|
|
||||||
|
## 2.5.4
|
||||||
|
|
||||||
|
## Security
|
||||||
|
- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem
|
||||||
|
|
||||||
|
## 2.5.3
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Emoji pack loader sanitizes pack names
|
||||||
|
- Reduced permissions of config files and directories, distros requiring greater permissions like group-read need to pre-create the directories
|
||||||
|
|
||||||
|
## 2.5.5
|
||||||
|
|
||||||
|
## Security
|
||||||
|
- Prevent users from accessing media of other users by creating a status with reused attachment ID
|
||||||
|
|
||||||
## 2.5.4
|
## 2.5.4
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,20 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Mix.Tasks.Pleroma.Benchmark do
|
defmodule Mix.Tasks.Pleroma.Benchmark do
|
||||||
import Mix.Pleroma
|
@shortdoc "Benchmarks"
|
||||||
|
@moduledoc """
|
||||||
|
Benchmark tasks available:
|
||||||
|
|
||||||
|
adapters
|
||||||
|
render_timeline
|
||||||
|
search
|
||||||
|
tag
|
||||||
|
|
||||||
|
MIX_ENV=benchmark mix pleroma.benchmark adapters
|
||||||
|
"""
|
||||||
|
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
|
import Mix.Pleroma
|
||||||
|
|
||||||
def run(["search"]) do
|
def run(["search"]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
@ -63,7 +75,7 @@ def run(["render_timeline", nickname | _] = args) do
|
||||||
|
|
||||||
Benchee.run(
|
Benchee.run(
|
||||||
%{
|
%{
|
||||||
"Standart rendering" => fn activities ->
|
"Standard rendering" => fn activities ->
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||||
activities: activities,
|
activities: activities,
|
||||||
for: user,
|
for: user,
|
|
@ -1 +0,0 @@
|
||||||
MediaProxy responses now return a sandbox CSP header
|
|
|
@ -1 +0,0 @@
|
||||||
Filter context activities using Visibility.visible_for_user?
|
|
|
@ -1 +0,0 @@
|
||||||
Add OAuth scope descriptions
|
|
|
@ -1 +0,0 @@
|
||||||
remove BBS/SSH feature, replaced by an external bridge.
|
|
|
@ -1 +0,0 @@
|
||||||
UploadedMedia: Add missing disposition_type to Content-Disposition
|
|
|
@ -1 +0,0 @@
|
||||||
Remove a few unused indexes.
|
|
|
@ -1 +0,0 @@
|
||||||
Cleanup OStatus-era user upgrades and ap_enabled indicator
|
|
|
@ -1 +0,0 @@
|
||||||
Allow lang attribute in status text
|
|
|
@ -1 +0,0 @@
|
||||||
Fix abnormal behaviour when refetching a poll
|
|
|
@ -1 +0,0 @@
|
||||||
Allow non-HTTP(s) URIs in "url" fields for compatibility with "FEP-fffd: Proxy Objects"
|
|
|
@ -1 +0,0 @@
|
||||||
Fix opengraph and twitter card meta tags
|
|
|
@ -1 +0,0 @@
|
||||||
ForceMentionsInContent: fix double mentions for Mastodon/Misskey posts
|
|
|
@ -1 +0,0 @@
|
||||||
OEmbed HTML tags are now filtered
|
|
|
@ -1 +0,0 @@
|
||||||
OnlyMedia Upload Filter
|
|
|
@ -1 +0,0 @@
|
||||||
Preload: Make generated JSON html-safe. It already was html safe because it only consists of config data that is base64 encoded, but this will keep it safe it that ever changes.
|
|
|
@ -1 +0,0 @@
|
||||||
Restrict attachments to only uploaded files only
|
|
|
@ -0,0 +1 @@
|
||||||
|
CommonAPI: Prevent users from accessing media of other users by creating a status with reused attachment ID
|
|
@ -1 +0,0 @@
|
||||||
Fix error 404 when deleting status of a banned user
|
|
|
@ -1 +0,0 @@
|
||||||
Deprecate Pleroma's audio scrobbling
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix the processing of email digest jobs.
|
|
@ -1 +0,0 @@
|
||||||
Disable XML entity resolution completely to fix a dos vulnerability
|
|
|
@ -1 +0,0 @@
|
||||||
- Fix config ownership in dockerfile to pass restriction test
|
|
|
@ -1 +0,0 @@
|
||||||
Implement MRF policy to reject or delist according to emojis
|
|
|
@ -1 +0,0 @@
|
||||||
Fix user fetch completely broken if featured collection is not in a supported form
|
|
|
@ -1 +0,0 @@
|
||||||
Correctly handle the situation when a poll has both "anyOf" and "oneOf" but one of them being empty
|
|
|
@ -1 +0,0 @@
|
||||||
Fix handling report from a deactivated user
|
|
|
@ -1 +0,0 @@
|
||||||
(hardening) Add no_new_privs=yes to OpenRC service files
|
|
|
@ -1 +0,0 @@
|
||||||
Prevent using the .json format to bypass authorized fetch mode
|
|
|
@ -1 +0,0 @@
|
||||||
Show more informative errors when profile exceeds char limits
|
|
|
@ -434,6 +434,8 @@
|
||||||
|
|
||||||
config :pleroma, :mrf_follow_bot, follower_nickname: nil
|
config :pleroma, :mrf_follow_bot, follower_nickname: nil
|
||||||
|
|
||||||
|
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
|
||||||
|
|
||||||
config :pleroma, :rich_media,
|
config :pleroma, :rich_media,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
ignore_hosts: [],
|
ignore_hosts: [],
|
||||||
|
@ -588,7 +590,6 @@
|
||||||
background: 5,
|
background: 5,
|
||||||
remote_fetcher: 2,
|
remote_fetcher: 2,
|
||||||
attachments_cleanup: 1,
|
attachments_cleanup: 1,
|
||||||
new_users_digest: 1,
|
|
||||||
mute_expire: 5
|
mute_expire: 5
|
||||||
],
|
],
|
||||||
plugins: [Oban.Plugins.Pruner],
|
plugins: [Oban.Plugins.Pruner],
|
||||||
|
@ -858,7 +859,11 @@
|
||||||
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
|
||||||
|
|
||||||
config :pleroma, :mrf,
|
config :pleroma, :mrf,
|
||||||
policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy],
|
policies: [
|
||||||
|
Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy,
|
||||||
|
Pleroma.Web.ActivityPub.MRF.TagPolicy,
|
||||||
|
Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
|
||||||
|
],
|
||||||
transparency: true,
|
transparency: true,
|
||||||
transparency_exclusions: []
|
transparency_exclusions: []
|
||||||
|
|
||||||
|
|
|
@ -160,6 +160,8 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
|
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
|
||||||
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
||||||
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
|
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
|
||||||
|
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
|
||||||
|
* `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
|
||||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||||
|
|
||||||
|
@ -267,6 +269,9 @@ Notes:
|
||||||
* `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
|
* `federated_timeline_removal_url`: A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
|
||||||
* `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
|
* `federated_timeline_removal_shortcode`: A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses. Each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html).
|
||||||
|
|
||||||
|
#### :mrf_inline_quote
|
||||||
|
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `<bdi>RT:</bdi> {url}`
|
||||||
|
|
||||||
### :activitypub
|
### :activitypub
|
||||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||||
|
|
|
@ -357,6 +357,122 @@ The message payload consist of:
|
||||||
- `follower_count`: follower count
|
- `follower_count`: follower count
|
||||||
- `following_count`: following count
|
- `following_count`: following count
|
||||||
|
|
||||||
|
### Authenticating via `sec-websocket-protocol` header
|
||||||
|
|
||||||
|
Pleroma allows to authenticate via the `sec-websocket-protocol` header, for example, if your access token is `your-access-token`, you can authenticate using the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
sec-websocket-protocol: your-access-token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authenticating after connection via `pleroma:authenticate` event
|
||||||
|
|
||||||
|
Pleroma allows to authenticate after connection is established, via the `pleroma:authenticate` event. For example, if your access token is `your-access-token`, you can send the following after the connection is established:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"type": "pleroma:authenticate", "token": "your-access-token"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response to client-sent events
|
||||||
|
|
||||||
|
Pleroma will respond to client-sent events that it recognizes. Supported event types are:
|
||||||
|
|
||||||
|
- `subscribe`
|
||||||
|
- `unsubscribe`
|
||||||
|
- `pleroma:authenticate`
|
||||||
|
|
||||||
|
The reply will be in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"event": "pleroma:respond",
|
||||||
|
"payload": "{\"type\": \"<type of the client-sent event>\", \"result\": \"<result of the action>\", \"error\": \"<error code>\"}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Result of the action can be either `success`, `ignored` or `error`. If it is `error`, the `error` property will contain the error code. Otherwise, the `error` property will not be present. Below are some examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"event": "pleroma:respond",
|
||||||
|
"payload": "{\"type\": \"pleroma:authenticate\", \"result\": \"success\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"event": "pleroma:respond",
|
||||||
|
"payload": "{\"type\": \"subscribe\", \"result\": \"ignored\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"event": "pleroma:respond",
|
||||||
|
"payload": "{\"type\": \"unsubscribe\", \"result\": \"error\", \"error\": \"bad_topic\"}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the sent event is not of a type that Pleroma supports, it will not reply.
|
||||||
|
|
||||||
|
### The `stream` attribute of a server-sent event
|
||||||
|
|
||||||
|
Technically, this is in Mastodon, but its documentation does nothing to specify its format.
|
||||||
|
|
||||||
|
This attribute appears on every event type except `pleroma:respond` and `delete`. It helps clients determine where they should display the new statuses.
|
||||||
|
|
||||||
|
The value of the attribute is an array containing one or two elements. The first element is the type of the stream. The second is the identifier related to that specific stream, if applicable.
|
||||||
|
|
||||||
|
For the following stream types, there is a second element in the array:
|
||||||
|
|
||||||
|
- `list`: The second element is the id of the list, as a string.
|
||||||
|
- `hashtag`: The second element is the name of the hashtag.
|
||||||
|
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
|
||||||
|
|
||||||
|
For all other stream types, there is no second element.
|
||||||
|
|
||||||
|
Some examples of valid `stream` values:
|
||||||
|
|
||||||
|
- `["list", "1"]`: List of id 1.
|
||||||
|
- `["hashtag", "mew"]`: The hashtag #mew.
|
||||||
|
- `["user:notifications"]`: Notifications for the current user.
|
||||||
|
- `["user"]`: Home timeline.
|
||||||
|
- `["public:remote", "mew.moe"]`: Public posts from the instance mew.moe .
|
||||||
|
|
||||||
|
### The unified streaming endpoint
|
||||||
|
|
||||||
|
If you do not specify a stream to connect to when requesting `/api/v1/streaming`, you will enter a connection that subscribes to no streams. After the connection is established, you can authenticate and then subscribe to different streams.
|
||||||
|
|
||||||
|
### List of supported streams
|
||||||
|
|
||||||
|
Below is a list of supported streams by Pleroma. To make a single-stream WebSocket connection, append the string specified in "Query style" to the streaming endpoint url.
|
||||||
|
To subscribe to a stream after the connection is established, merge the JSON object specified in "Subscribe style" with `{"type": "subscribe"}`. To unsubscribe, merge it with `{"type": "unsubscribe"}`.
|
||||||
|
|
||||||
|
For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send
|
||||||
|
|
||||||
|
```
|
||||||
|
{"type": "subscribe", "stream": "list", "list": "1"}
|
||||||
|
```
|
||||||
|
|
||||||
|
upon establishing the websocket connection.
|
||||||
|
|
||||||
|
To unsubscribe to list 1, send
|
||||||
|
|
||||||
|
```
|
||||||
|
{"type": "unsubscribe", "stream": "list", "list": "1"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header.
|
||||||
|
|
||||||
|
- `list`
|
||||||
|
- Query style: `?stream=list&list=<id>`
|
||||||
|
- Subscribe style: `{"stream": "list", "list": "<id>"}`
|
||||||
|
- `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct`
|
||||||
|
- Query style: `?stream=<stream name>`
|
||||||
|
- Subscribe style: `{"stream": "<stream name>"}`
|
||||||
|
- `hashtag`
|
||||||
|
- Query style: `?stream=hashtag&tag=<name>`
|
||||||
|
- Subscribe style: `{"stream": "hashtag", "tag": "<name>"}`
|
||||||
|
- `public:remote`, `public:remote:media`
|
||||||
|
- Query style: `?stream=<stream name>&instance=<instance domain>`
|
||||||
|
- Subscribe style: `{"stream": "<stream name>", "instance": "<instance domain>"}`
|
||||||
|
|
||||||
## User muting and thread muting
|
## User muting and thread muting
|
||||||
|
|
||||||
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
|
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
|
||||||
|
|
|
@ -83,4 +83,19 @@ defmodule Pleroma.Constants do
|
||||||
)
|
)
|
||||||
|
|
||||||
const(upload_object_types, do: ["Document", "Image"])
|
const(upload_object_types, do: ["Document", "Image"])
|
||||||
|
|
||||||
|
const(activity_json_canonical_mime_type,
|
||||||
|
do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||||
|
)
|
||||||
|
|
||||||
|
const(activity_json_mime_types,
|
||||||
|
do: [
|
||||||
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
|
"application/activity+json"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const(public_streams,
|
||||||
|
do: ["public", "public:local", "public:media", "public:local:media"]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -124,7 +124,7 @@ def mentions_escape(text, options \\ []) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def markdown_to_html(text) do
|
def markdown_to_html(text) do
|
||||||
Earmark.as_html!(text, %Earmark.Options{compact_output: true})
|
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
|
||||||
end
|
end
|
||||||
|
|
||||||
def html_escape({text, mentions, hashtags}, type) do
|
def html_escape({text, mentions, hashtags}, type) do
|
||||||
|
|
|
@ -40,7 +40,11 @@ defp with_media_attachments(
|
||||||
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
||||||
)
|
)
|
||||||
when is_list(media_ids) do
|
when is_list(media_ids) do
|
||||||
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
|
media_attachments =
|
||||||
|
Utils.attachments_from_ids(
|
||||||
|
%{media_ids: media_ids},
|
||||||
|
User.get_cached_by_id(changeset.data.user_id)
|
||||||
|
)
|
||||||
|
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|
|
|
@ -217,6 +217,7 @@ def note(%ActivityDraft{} = draft) do
|
||||||
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
|
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
|
||||||
}
|
}
|
||||||
|> add_in_reply_to(draft.in_reply_to)
|
|> add_in_reply_to(draft.in_reply_to)
|
||||||
|
|> add_quote(draft.quote_post)
|
||||||
|> Map.merge(draft.extra)
|
|> Map.merge(draft.extra)
|
||||||
|
|
||||||
{:ok, data, []}
|
{:ok, data, []}
|
||||||
|
@ -232,6 +233,16 @@ defp add_in_reply_to(object, in_reply_to) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp add_quote(object, nil), do: object
|
||||||
|
|
||||||
|
defp add_quote(object, quote_post) do
|
||||||
|
with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
|
||||||
|
Map.put(object, "quoteUrl", quote_object.data["id"])
|
||||||
|
else
|
||||||
|
_ -> object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def chat_message(actor, recipient, content, opts \\ []) do
|
def chat_message(actor, recipient, content, opts \\ []) do
|
||||||
basic = %{
|
basic = %{
|
||||||
"id" => Utils.generate_object_id(),
|
"id" => Utils.generate_object_id(),
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
||||||
|
@moduledoc "Force a quote line into the message content."
|
||||||
|
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
|
||||||
|
defp build_inline_quote(template, url) do
|
||||||
|
quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>")
|
||||||
|
|
||||||
|
"<span class=\"quote-inline\"><br/><br/>#{quote_line}</span>"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp has_inline_quote?(content, quote_url) do
|
||||||
|
cond do
|
||||||
|
# Does the quote URL exist in the content?
|
||||||
|
content =~ quote_url -> true
|
||||||
|
# Does the content already have a .quote-inline span?
|
||||||
|
content =~ "<span class=\"quote-inline\">" -> true
|
||||||
|
# No inline quote found
|
||||||
|
true -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||||
|
content = object["content"] || ""
|
||||||
|
|
||||||
|
if has_inline_quote?(content, quote_url) do
|
||||||
|
object
|
||||||
|
else
|
||||||
|
template = Pleroma.Config.get([:mrf_inline_quote, :template])
|
||||||
|
|
||||||
|
content =
|
||||||
|
if String.ends_with?(content, "</p>"),
|
||||||
|
do:
|
||||||
|
String.trim_trailing(content, "</p>") <>
|
||||||
|
build_inline_quote(template, quote_url) <> "</p>",
|
||||||
|
else: content <> build_inline_quote(template, quote_url)
|
||||||
|
|
||||||
|
Map.put(object, "content", content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||||
|
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def filter(object), do: {:ok, object}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def describe, do: {:ok, %{}}
|
||||||
|
|
||||||
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
def history_awareness, do: :auto
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def config_description do
|
||||||
|
%{
|
||||||
|
key: :mrf_inline_quote,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
|
||||||
|
label: "MRF Inline Quote Policy",
|
||||||
|
type: :group,
|
||||||
|
description: "Force quote url to appear in post content.",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :template,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
|
||||||
|
suggestions: ["<bdi>RT:</bdi> {url}"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
|
||||||
|
@moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
|
||||||
|
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||||
|
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
def filter(object), do: {:ok, object}
|
||||||
|
|
||||||
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
def describe, do: {:ok, %{}}
|
||||||
|
|
||||||
|
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
def history_awareness, do: :auto
|
||||||
|
|
||||||
|
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||||
|
tags = object["tag"] || []
|
||||||
|
|
||||||
|
if Enum.any?(tags, fn tag ->
|
||||||
|
CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url
|
||||||
|
end) do
|
||||||
|
object
|
||||||
|
else
|
||||||
|
object
|
||||||
|
|> Map.put(
|
||||||
|
"tag",
|
||||||
|
tags ++
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
"type" => "Link",
|
||||||
|
"mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(),
|
||||||
|
"href" => quote_url
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -84,6 +84,7 @@ defp fix(data) do
|
||||||
|> fix_tag()
|
|> fix_tag()
|
||||||
|> fix_replies()
|
|> fix_replies()
|
||||||
|> fix_attachments()
|
|> fix_attachments()
|
||||||
|
|> CommonFixes.fix_quote_url()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|> Transmogrifier.fix_content_map()
|
|> Transmogrifier.fix_content_map()
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,6 +99,7 @@ defp fix(data) do
|
||||||
data
|
data
|
||||||
|> CommonFixes.fix_actor()
|
|> CommonFixes.fix_actor()
|
||||||
|> CommonFixes.fix_object_defaults()
|
|> CommonFixes.fix_object_defaults()
|
||||||
|
|> CommonFixes.fix_quote_url()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|> fix_url()
|
|> fix_url()
|
||||||
|> fix_content()
|
|> fix_content()
|
||||||
|
|
|
@ -27,7 +27,7 @@ defmacro activity_fields do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# All objects except Answer and CHatMessage
|
# All objects except Answer and ChatMessage
|
||||||
defmacro object_fields do
|
defmacro object_fields do
|
||||||
quote bind_quoted: binding() do
|
quote bind_quoted: binding() do
|
||||||
field(:content, :string)
|
field(:content, :string)
|
||||||
|
@ -58,6 +58,7 @@ defmacro status_object_fields do
|
||||||
field(:like_count, :integer, default: 0)
|
field(:like_count, :integer, default: 0)
|
||||||
field(:announcement_count, :integer, default: 0)
|
field(:announcement_count, :integer, default: 0)
|
||||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||||
|
field(:quoteUrl, ObjectValidators.ObjectID)
|
||||||
field(:url, ObjectValidators.BareUri)
|
field(:url, ObjectValidators.BareUri)
|
||||||
|
|
||||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||||
|
|
|
@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
||||||
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
|
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
|
||||||
|
|
||||||
|
@ -76,4 +78,48 @@ def fix_object_action_recipients(data, %Object{data: %{"actor" => actor}}) do
|
||||||
|
|
||||||
Map.put(data, "to", to)
|
Map.put(data, "to", to)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
|
||||||
|
|
||||||
|
# Fedibird
|
||||||
|
# https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||||
|
def fix_quote_url(%{"quoteUri" => quote_url} = data) do
|
||||||
|
Map.put(data, "quoteUrl", quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Old Fedibird (bug)
|
||||||
|
# https://github.com/fedibird/mastodon/issues/9
|
||||||
|
def fix_quote_url(%{"quoteURL" => quote_url} = data) do
|
||||||
|
Map.put(data, "quoteUrl", quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Misskey fallback
|
||||||
|
def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
|
||||||
|
Map.put(data, "quoteUrl", quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
|
||||||
|
tag = Enum.find(tags, &is_object_link_tag/1)
|
||||||
|
|
||||||
|
if not is_nil(tag) do
|
||||||
|
data
|
||||||
|
|> Map.put("quoteUrl", tag["href"])
|
||||||
|
else
|
||||||
|
data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_quote_url(data), do: data
|
||||||
|
|
||||||
|
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
|
||||||
|
def is_object_link_tag(%{
|
||||||
|
"type" => "Link",
|
||||||
|
"mediaType" => media_type,
|
||||||
|
"href" => href
|
||||||
|
})
|
||||||
|
when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_object_link_tag(_), do: false
|
||||||
end
|
end
|
||||||
|
|
|
@ -62,6 +62,7 @@ defp fix(data) do
|
||||||
data
|
data
|
||||||
|> CommonFixes.fix_actor()
|
|> CommonFixes.fix_actor()
|
||||||
|> CommonFixes.fix_object_defaults()
|
|> CommonFixes.fix_object_defaults()
|
||||||
|
|> CommonFixes.fix_quote_url()
|
||||||
|> Transmogrifier.fix_emoji()
|
|> Transmogrifier.fix_emoji()
|
||||||
|> fix_closed()
|
|> fix_closed()
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@primary_key false
|
@primary_key false
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
# Common
|
# Common
|
||||||
field(:type, :string)
|
field(:type, :string)
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
|
|
||||||
# Mention, Hashtag
|
# Mention, Hashtag, Link
|
||||||
field(:href, ObjectValidators.Uri)
|
field(:href, ObjectValidators.Uri)
|
||||||
|
|
||||||
|
# Link
|
||||||
|
field(:mediaType, :string)
|
||||||
|
|
||||||
# Emoji
|
# Emoji
|
||||||
embeds_one :icon, IconObjectValidator, primary_key: false do
|
embeds_one :icon, IconObjectValidator, primary_key: false do
|
||||||
field(:type, :string)
|
field(:type, :string)
|
||||||
|
@ -68,6 +73,13 @@ def changeset(struct, %{"type" => "Emoji"} = data) do
|
||||||
|> validate_required([:type, :name, :icon])
|
|> validate_required([:type, :name, :icon])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def changeset(struct, %{"type" => "Link"} = data) do
|
||||||
|
struct
|
||||||
|
|> cast(data, [:type, :name, :mediaType, :href])
|
||||||
|
|> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types())
|
||||||
|
|> validate_required([:type, :href, :mediaType])
|
||||||
|
end
|
||||||
|
|
||||||
def changeset(struct, %{"type" => _} = data) do
|
def changeset(struct, %{"type" => _} = data) do
|
||||||
struct
|
struct
|
||||||
|> cast(data, [])
|
|> cast(data, [])
|
||||||
|
|
|
@ -166,6 +166,27 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
|
||||||
|
|
||||||
def fix_in_reply_to(object, _options), do: object
|
def fix_in_reply_to(object, _options), do: object
|
||||||
|
|
||||||
|
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
||||||
|
quote_url =
|
||||||
|
case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
|
||||||
|
%{"quoteUrl" => quote_url} -> quote_url
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
|
||||||
|
{:ok, quoted_object} <- get_obj_helper(quote_url, options),
|
||||||
|
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
|
||||||
|
Map.put(object, "quoteUrl", quoted_object.data["id"])
|
||||||
|
else
|
||||||
|
{:quoting?, _} ->
|
||||||
|
object
|
||||||
|
|
||||||
|
e ->
|
||||||
|
Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp prepare_in_reply_to(in_reply_to) do
|
defp prepare_in_reply_to(in_reply_to) do
|
||||||
cond do
|
cond do
|
||||||
is_bitstring(in_reply_to) ->
|
is_bitstring(in_reply_to) ->
|
||||||
|
@ -454,6 +475,7 @@ def handle_incoming(
|
||||||
|> strip_internal_fields()
|
|> strip_internal_fields()
|
||||||
|> fix_type(fetch_options)
|
|> fix_type(fetch_options)
|
||||||
|> fix_in_reply_to(fetch_options)
|
|> fix_in_reply_to(fetch_options)
|
||||||
|
|> fix_quote_url_and_maybe_fetch(fetch_options)
|
||||||
|
|
||||||
data = Map.put(data, "object", object)
|
data = Map.put(data, "object", object)
|
||||||
options = Keyword.put(options, :local, false)
|
options = Keyword.put(options, :local, false)
|
||||||
|
@ -628,6 +650,16 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
|
||||||
|
|
||||||
def set_reply_to_uri(obj), do: obj
|
def set_reply_to_uri(obj), do: obj
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fedibird compatibility
|
||||||
|
https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||||
|
"""
|
||||||
|
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
|
||||||
|
Map.put(object, "quoteUri", quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_quote_url(obj), do: obj
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
||||||
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
||||||
|
@ -682,6 +714,7 @@ def prepare_object(object) do
|
||||||
|> prepare_attachments
|
|> prepare_attachments
|
||||||
|> set_conversation
|
|> set_conversation
|
||||||
|> set_reply_to_uri
|
|> set_reply_to_uri
|
||||||
|
|> set_quote_url
|
||||||
|> set_replies
|
|> set_replies
|
||||||
|> strip_internal_fields
|
|> strip_internal_fields
|
||||||
|> strip_internal_tags
|
|> strip_internal_tags
|
||||||
|
|
|
@ -10,6 +10,14 @@ defmodule Pleroma.Web.ApiSpec do
|
||||||
|
|
||||||
@behaviour OpenApi
|
@behaviour OpenApi
|
||||||
|
|
||||||
|
defp streaming_paths do
|
||||||
|
%{
|
||||||
|
"/api/v1/streaming" => %OpenApiSpex.PathItem{
|
||||||
|
get: Pleroma.Web.ApiSpec.StreamingOperation.streaming_operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
@impl OpenApi
|
@impl OpenApi
|
||||||
def spec(opts \\ []) do
|
def spec(opts \\ []) do
|
||||||
%OpenApi{
|
%OpenApi{
|
||||||
|
@ -45,7 +53,7 @@ def spec(opts \\ []) do
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# populate the paths from a phoenix router
|
# populate the paths from a phoenix router
|
||||||
paths: OpenApiSpex.Paths.from_router(Router),
|
paths: Map.merge(streaming_paths(), OpenApiSpex.Paths.from_router(Router)),
|
||||||
components: %OpenApiSpex.Components{
|
components: %OpenApiSpex.Components{
|
||||||
parameters: %{
|
parameters: %{
|
||||||
"accountIdOrNickname" =>
|
"accountIdOrNickname" =>
|
||||||
|
|
|
@ -581,6 +581,11 @@ defp create_request do
|
||||||
type: :string,
|
type: :string,
|
||||||
description:
|
description:
|
||||||
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
|
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
|
||||||
|
},
|
||||||
|
quote_id: %Schema{
|
||||||
|
nullable: true,
|
||||||
|
allOf: [FlakeID],
|
||||||
|
description: "ID of the status being quoted, if any"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
|
|
|
@ -0,0 +1,464 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.StreamingOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Response
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.NotificationOperation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@spec open_api_operation(atom) :: Operation.t()
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec streaming_operation() :: Operation.t()
|
||||||
|
def streaming_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Timelines"],
|
||||||
|
summary: "Establish streaming connection",
|
||||||
|
description: """
|
||||||
|
Receive statuses in real-time via WebSocket.
|
||||||
|
|
||||||
|
You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
|
||||||
|
the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
|
||||||
|
your client's compatibility with Mastodon).
|
||||||
|
|
||||||
|
You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users,
|
||||||
|
you must specify the access token at the time of the connection (i.e. via query string or header).
|
||||||
|
|
||||||
|
Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
|
||||||
|
|
||||||
|
The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
|
||||||
|
describes what events server will send through WebSocket.
|
||||||
|
""",
|
||||||
|
security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
|
||||||
|
operationId: "WebsocketHandler.streaming",
|
||||||
|
parameters:
|
||||||
|
[
|
||||||
|
Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:"sec-websocket-key",
|
||||||
|
:header,
|
||||||
|
%Schema{type: :string},
|
||||||
|
"sec-websocket-key header",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:"sec-websocket-version",
|
||||||
|
:header,
|
||||||
|
%Schema{type: :string},
|
||||||
|
"sec-websocket-version header",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
] ++ stream_params() ++ access_token_params(),
|
||||||
|
requestBody: request_body("Client-sent events", client_sent_events()),
|
||||||
|
responses: %{
|
||||||
|
101 => switching_protocols_response(),
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"Server-sent events",
|
||||||
|
"application/json",
|
||||||
|
server_sent_events()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp stream_params do
|
||||||
|
stream_specifier()
|
||||||
|
|> Enum.map(fn {name, schema} ->
|
||||||
|
Operation.parameter(name, :query, schema, get_schema(schema).description)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp access_token_params do
|
||||||
|
[
|
||||||
|
Operation.parameter(:access_token, :query, token(), token().description),
|
||||||
|
Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp switching_protocols_response do
|
||||||
|
%Response{
|
||||||
|
description: "Switching protocols",
|
||||||
|
headers: %{
|
||||||
|
"connection" => %OpenApiSpex.Header{required: true},
|
||||||
|
"upgrade" => %OpenApiSpex.Header{required: true},
|
||||||
|
"sec-websocket-accept" => %OpenApiSpex.Header{required: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp server_sent_events do
|
||||||
|
%Schema{
|
||||||
|
oneOf: [
|
||||||
|
update_event(),
|
||||||
|
status_update_event(),
|
||||||
|
notification_event(),
|
||||||
|
chat_update_event(),
|
||||||
|
follow_relationships_update_event(),
|
||||||
|
conversation_event(),
|
||||||
|
delete_event(),
|
||||||
|
pleroma_respond_event()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp stream do
|
||||||
|
%Schema{
|
||||||
|
type: :array,
|
||||||
|
title: "Stream",
|
||||||
|
description: """
|
||||||
|
The stream identifier.
|
||||||
|
The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
|
||||||
|
Currently, for the following stream types, there is a second element in the array:
|
||||||
|
|
||||||
|
- `list`: The second element is the id of the list, as a string.
|
||||||
|
- `hashtag`: The second element is the name of the hashtag.
|
||||||
|
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
|
||||||
|
""",
|
||||||
|
maxItems: 2,
|
||||||
|
minItems: 1,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
example: ["hashtag", "mew"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_schema(%Schema{} = schema), do: schema
|
||||||
|
defp get_schema(schema), do: schema.schema
|
||||||
|
|
||||||
|
defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
|
||||||
|
payload_type = Keyword.get(opts, :payload_type, :json)
|
||||||
|
has_stream = Keyword.get(opts, :has_stream, true)
|
||||||
|
|
||||||
|
stream_properties =
|
||||||
|
if has_stream do
|
||||||
|
%{stream: stream()}
|
||||||
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|
||||||
|
stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
|
||||||
|
|
||||||
|
stream_required = if has_stream, do: [:stream], else: []
|
||||||
|
|
||||||
|
payload_schema =
|
||||||
|
if payload_type == :json do
|
||||||
|
%Schema{
|
||||||
|
title: "Event payload",
|
||||||
|
description: "JSON-encoded string of #{get_schema(payload).title}",
|
||||||
|
allOf: [payload]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
payload
|
||||||
|
end
|
||||||
|
|
||||||
|
payload_example =
|
||||||
|
if payload_type == :json do
|
||||||
|
get_schema(payload).example |> Jason.encode!()
|
||||||
|
else
|
||||||
|
get_schema(payload).example
|
||||||
|
end
|
||||||
|
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
title: name,
|
||||||
|
description: description,
|
||||||
|
required: [:event, :payload] ++ stream_required,
|
||||||
|
properties:
|
||||||
|
%{
|
||||||
|
event: %Schema{
|
||||||
|
title: "Event type",
|
||||||
|
description: "Type of the event.",
|
||||||
|
type: :string,
|
||||||
|
required: true,
|
||||||
|
enum: [type]
|
||||||
|
},
|
||||||
|
payload: payload_schema
|
||||||
|
}
|
||||||
|
|> Map.merge(stream_properties),
|
||||||
|
example:
|
||||||
|
%{
|
||||||
|
"event" => type,
|
||||||
|
"payload" => payload_example
|
||||||
|
}
|
||||||
|
|> Map.merge(stream_example)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_event do
|
||||||
|
server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_update_event do
|
||||||
|
server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notification_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Notification",
|
||||||
|
"A new notification.",
|
||||||
|
"notification",
|
||||||
|
NotificationOperation.notification()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp follow_relationships_update_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Follow relationships update",
|
||||||
|
"An update to follow relationships.",
|
||||||
|
"pleroma:follow_relationships_update",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
title: "Follow relationships update",
|
||||||
|
required: [:state, :follower, :following],
|
||||||
|
properties: %{
|
||||||
|
state: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Follow state of the relationship.",
|
||||||
|
enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
|
||||||
|
},
|
||||||
|
follower: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "Information about the follower.",
|
||||||
|
required: [:id, :follower_count, :following_count],
|
||||||
|
properties: %{
|
||||||
|
id: FlakeID,
|
||||||
|
follower_count: %Schema{type: :integer},
|
||||||
|
following_count: %Schema{type: :integer}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
following: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "Information about the following person.",
|
||||||
|
required: [:id, :follower_count, :following_count],
|
||||||
|
properties: %{
|
||||||
|
id: FlakeID,
|
||||||
|
follower_count: %Schema{type: :integer},
|
||||||
|
following_count: %Schema{type: :integer}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"state" => "follow_pending",
|
||||||
|
"follower" => %{
|
||||||
|
"id" => "someUser1",
|
||||||
|
"follower_count" => 1,
|
||||||
|
"following_count" => 1
|
||||||
|
},
|
||||||
|
"following" => %{
|
||||||
|
"id" => "someUser2",
|
||||||
|
"follower_count" => 1,
|
||||||
|
"following_count" => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp chat_update_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Chat update",
|
||||||
|
"A new chat message.",
|
||||||
|
"pleroma:chat_update",
|
||||||
|
Chat
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp conversation_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Conversation update",
|
||||||
|
"An update about a conversation",
|
||||||
|
"conversation",
|
||||||
|
Conversation
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Delete",
|
||||||
|
"A status that was just deleted.",
|
||||||
|
"delete",
|
||||||
|
%Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "Status id",
|
||||||
|
description: "Id of the deleted status",
|
||||||
|
allOf: [FlakeID],
|
||||||
|
example: "some-opaque-id"
|
||||||
|
},
|
||||||
|
payload_type: :string,
|
||||||
|
has_stream: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pleroma_respond_event do
|
||||||
|
server_sent_event_helper(
|
||||||
|
"Server response",
|
||||||
|
"A response to a client-sent event.",
|
||||||
|
"pleroma:respond",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
title: "Results",
|
||||||
|
required: [:result, :type],
|
||||||
|
properties: %{
|
||||||
|
result: %Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "Result of the request",
|
||||||
|
enum: ["success", "error", "ignored"]
|
||||||
|
},
|
||||||
|
error: %Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "Error code",
|
||||||
|
description: "An error identifier. Only appears if `result` is `error`."
|
||||||
|
},
|
||||||
|
type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Type of the request."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{"result" => "success", "type" => "pleroma:authenticate"}
|
||||||
|
},
|
||||||
|
has_stream: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_sent_events do
|
||||||
|
%Schema{
|
||||||
|
oneOf: [
|
||||||
|
subscribe_event(),
|
||||||
|
unsubscribe_event(),
|
||||||
|
authenticate_event()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp request_body(description, schema, opts \\ []) do
|
||||||
|
%OpenApiSpex.RequestBody{
|
||||||
|
description: description,
|
||||||
|
content: %{
|
||||||
|
"application/json" => %OpenApiSpex.MediaType{
|
||||||
|
schema: schema,
|
||||||
|
example: opts[:example],
|
||||||
|
examples: opts[:examples]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_sent_event_helper(name, description, type, properties, opts) do
|
||||||
|
required = opts[:required] || []
|
||||||
|
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
title: name,
|
||||||
|
required: [:type] ++ required,
|
||||||
|
description: description,
|
||||||
|
properties:
|
||||||
|
%{
|
||||||
|
type: %Schema{type: :string, enum: [type], description: "Type of the event."}
|
||||||
|
}
|
||||||
|
|> Map.merge(properties),
|
||||||
|
example: opts[:example]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp subscribe_event do
|
||||||
|
client_sent_event_helper(
|
||||||
|
"Subscribe",
|
||||||
|
"Subscribe to a stream.",
|
||||||
|
"subscribe",
|
||||||
|
stream_specifier(),
|
||||||
|
required: [:stream],
|
||||||
|
example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp unsubscribe_event do
|
||||||
|
client_sent_event_helper(
|
||||||
|
"Unsubscribe",
|
||||||
|
"Unsubscribe from a stream.",
|
||||||
|
"unsubscribe",
|
||||||
|
stream_specifier(),
|
||||||
|
required: [:stream],
|
||||||
|
example: %{
|
||||||
|
"type" => "unsubscribe",
|
||||||
|
"stream" => "public:remote:media",
|
||||||
|
"instance" => "example.org"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authenticate_event do
|
||||||
|
client_sent_event_helper(
|
||||||
|
"Authenticate",
|
||||||
|
"Authenticate via an access token.",
|
||||||
|
"pleroma:authenticate",
|
||||||
|
%{
|
||||||
|
token: token()
|
||||||
|
},
|
||||||
|
required: [:token]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp token do
|
||||||
|
%Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "An OAuth access token with corresponding permissions.",
|
||||||
|
example: "some token"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp stream_specifier do
|
||||||
|
%{
|
||||||
|
stream: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "The name of the stream.",
|
||||||
|
enum:
|
||||||
|
Pleroma.Constants.public_streams() ++
|
||||||
|
[
|
||||||
|
"public:remote",
|
||||||
|
"public:remote:media",
|
||||||
|
"user",
|
||||||
|
"user:pleroma_chat",
|
||||||
|
"user:notification",
|
||||||
|
"direct",
|
||||||
|
"list",
|
||||||
|
"hashtag"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
list: %Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "List id",
|
||||||
|
description: "The id of the list. Required when `stream` is `list`.",
|
||||||
|
example: "some-id"
|
||||||
|
},
|
||||||
|
tag: %Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "Hashtag name",
|
||||||
|
description: "The name of the hashtag. Required when `stream` is `hashtag`.",
|
||||||
|
example: "mew"
|
||||||
|
},
|
||||||
|
instance: %Schema{
|
||||||
|
type: :string,
|
||||||
|
title: "Domain name",
|
||||||
|
description:
|
||||||
|
"Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
|
||||||
|
example: "example.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -193,6 +193,26 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: "The `acct` property of User entity for replied user (if any)"
|
description: "The `acct` property of User entity for replied user (if any)"
|
||||||
},
|
},
|
||||||
|
quote: %Schema{
|
||||||
|
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
|
||||||
|
nullable: true,
|
||||||
|
description: "Quoted status (if any)"
|
||||||
|
},
|
||||||
|
quote_id: %Schema{
|
||||||
|
nullable: true,
|
||||||
|
allOf: [FlakeID],
|
||||||
|
description: "ID of the status being quoted, if any"
|
||||||
|
},
|
||||||
|
quote_url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
nullable: true,
|
||||||
|
description: "URL of the quoted status"
|
||||||
|
},
|
||||||
|
quote_visible: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "`true` if the quoted post is visible to the user"
|
||||||
|
},
|
||||||
local: %Schema{
|
local: %Schema{
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "`true` if the post was made on the local instance"
|
description: "`true` if the post was made on the local instance"
|
||||||
|
|
|
@ -33,6 +33,7 @@ def block(blocker, blocked) do
|
||||||
|
|
||||||
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
||||||
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
||||||
|
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
|
||||||
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
||||||
{_, {:ok, chat_message_data, _meta}} <-
|
{_, {:ok, chat_message_data, _meta}} <-
|
||||||
{:build_object,
|
{:build_object,
|
||||||
|
@ -71,6 +72,17 @@ defp format_chat_content(content) do
|
||||||
text
|
text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_chat_attachment_attribution(nil, _), do: :ok
|
||||||
|
|
||||||
|
defp validate_chat_attachment_attribution(attachment, user) do
|
||||||
|
with :ok <- Object.authorize_access(attachment, user) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp validate_chat_content_length(_, true), do: :ok
|
defp validate_chat_content_length(_, true), do: :ok
|
||||||
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
|
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
alias Pleroma.Conversation.Participation
|
alias Pleroma.Conversation.Participation
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Web.ActivityPub.Builder
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
|
|
||||||
import Pleroma.Web.Gettext
|
import Pleroma.Web.Gettext
|
||||||
|
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||||
|
|
||||||
defstruct valid?: true,
|
defstruct valid?: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
|
@ -22,6 +24,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
attachments: [],
|
attachments: [],
|
||||||
in_reply_to: nil,
|
in_reply_to: nil,
|
||||||
in_reply_to_conversation: nil,
|
in_reply_to_conversation: nil,
|
||||||
|
quote_post: nil,
|
||||||
visibility: nil,
|
visibility: nil,
|
||||||
expires_at: nil,
|
expires_at: nil,
|
||||||
extra: nil,
|
extra: nil,
|
||||||
|
@ -53,7 +56,9 @@ def create(user, params) do
|
||||||
|> poll()
|
|> poll()
|
||||||
|> with_valid(&in_reply_to/1)
|
|> with_valid(&in_reply_to/1)
|
||||||
|> with_valid(&in_reply_to_conversation/1)
|
|> with_valid(&in_reply_to_conversation/1)
|
||||||
|
|> with_valid("e_post/1)
|
||||||
|> with_valid(&visibility/1)
|
|> with_valid(&visibility/1)
|
||||||
|
|> with_valid("ing_visibility/1)
|
||||||
|> content()
|
|> content()
|
||||||
|> with_valid(&to_and_cc/1)
|
|> with_valid(&to_and_cc/1)
|
||||||
|> with_valid(&context/1)
|
|> with_valid(&context/1)
|
||||||
|
@ -111,7 +116,7 @@ defp full_payload(%{status: status, summary: summary} = draft) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attachments(%{params: params} = draft) do
|
defp attachments(%{params: params} = draft) do
|
||||||
attachments = Utils.attachments_from_ids(params)
|
attachments = Utils.attachments_from_ids(params, draft.user)
|
||||||
draft = %__MODULE__{draft | attachments: attachments}
|
draft = %__MODULE__{draft | attachments: attachments}
|
||||||
|
|
||||||
case Utils.validate_attachments_count(attachments) do
|
case Utils.validate_attachments_count(attachments) do
|
||||||
|
@ -132,6 +137,18 @@ defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}}
|
||||||
|
|
||||||
defp in_reply_to(draft), do: draft
|
defp in_reply_to(draft), do: draft
|
||||||
|
|
||||||
|
defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do
|
||||||
|
case Activity.get_by_id_with_object(id) do
|
||||||
|
%Activity{} = activity ->
|
||||||
|
%__MODULE__{draft | quote_post: activity}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
draft
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp quote_post(draft), do: draft
|
||||||
|
|
||||||
defp in_reply_to_conversation(draft) do
|
defp in_reply_to_conversation(draft) do
|
||||||
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
|
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
|
||||||
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
|
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
|
||||||
|
@ -147,6 +164,29 @@ defp visibility(%{params: params} = draft) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_quote?(draft, object, "private") do
|
||||||
|
draft.user.ap_id == object.data["actor"]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_quote?(_, _, _) do
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
defp quoting_visibility(%{quote_post: %Activity{}} = draft) do
|
||||||
|
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
|
||||||
|
true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
|
||||||
|
draft
|
||||||
|
else
|
||||||
|
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp quoting_visibility(draft), do: draft
|
||||||
|
|
||||||
defp expires_at(draft) do
|
defp expires_at(draft) do
|
||||||
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
|
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
|
||||||
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
|
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
|
||||||
|
@ -164,12 +204,15 @@ defp poll(draft) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp content(draft) do
|
defp content(%{mentions: mentions} = draft) do
|
||||||
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
|
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
|
||||||
|
|
||||||
|
mentioned_ap_ids =
|
||||||
|
Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
||||||
|
|
||||||
mentions =
|
mentions =
|
||||||
mentioned_users
|
mentions
|
||||||
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
|> Kernel.++(mentioned_ap_ids)
|
||||||
|> Utils.get_addressed_users(draft.params[:to])
|
|> Utils.get_addressed_users(draft.params[:to])
|
||||||
|
|
||||||
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
|
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
|
||||||
|
|
|
@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
|
def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do
|
||||||
attachments_from_ids_descs(ids, desc)
|
attachments_from_ids_descs(ids, desc, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids}) do
|
def attachments_from_ids(%{media_ids: ids}, user) do
|
||||||
attachments_from_ids_no_descs(ids)
|
attachments_from_ids_no_descs(ids, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(_), do: []
|
def attachments_from_ids(_, _), do: []
|
||||||
|
|
||||||
def attachments_from_ids_no_descs([]), do: []
|
def attachments_from_ids_no_descs([], _), do: []
|
||||||
|
|
||||||
def attachments_from_ids_no_descs(ids) do
|
def attachments_from_ids_no_descs(ids, user) do
|
||||||
Enum.map(ids, fn media_id ->
|
Enum.map(ids, fn media_id ->
|
||||||
case get_attachment(media_id) do
|
case get_attachment(media_id, user) do
|
||||||
%Object{data: data} -> data
|
%Object{data: data} -> data
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
@ -45,22 +45,23 @@ def attachments_from_ids_no_descs(ids) do
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids_descs([], _), do: []
|
def attachments_from_ids_descs([], _, _), do: []
|
||||||
|
|
||||||
def attachments_from_ids_descs(ids, descs_str) do
|
def attachments_from_ids_descs(ids, descs_str, user) do
|
||||||
{_, descs} = Jason.decode(descs_str)
|
{_, descs} = Jason.decode(descs_str)
|
||||||
|
|
||||||
Enum.map(ids, fn media_id ->
|
Enum.map(ids, fn media_id ->
|
||||||
with %Object{data: data} <- get_attachment(media_id) do
|
with %Object{data: data} <- get_attachment(media_id, user) do
|
||||||
Map.put(data, "name", descs[media_id])
|
Map.put(data, "name", descs[media_id])
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_attachment(media_id) do
|
defp get_attachment(media_id, user) do
|
||||||
with %Object{data: data} = object <- Repo.get(Object, media_id),
|
with %Object{data: data} = object <- Repo.get(Object, media_id),
|
||||||
%{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data do
|
%{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data,
|
||||||
|
:ok <- Object.authorize_access(object, user) do
|
||||||
object
|
object
|
||||||
else
|
else
|
||||||
_ -> nil
|
_ -> nil
|
||||||
|
|
|
@ -93,6 +93,7 @@ def features do
|
||||||
"multifetch",
|
"multifetch",
|
||||||
"pleroma:api/v1/notifications:include_types_filter",
|
"pleroma:api/v1/notifications:include_types_filter",
|
||||||
"editing",
|
"editing",
|
||||||
|
"quote_posting",
|
||||||
if Config.get([:activitypub, :blockers_visible]) do
|
if Config.get([:activitypub, :blockers_visible]) do
|
||||||
"blockers_visible"
|
"blockers_visible"
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -57,6 +57,27 @@ defp get_replied_to_activities(activities) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_quoted_activities([]), do: %{}
|
||||||
|
|
||||||
|
defp get_quoted_activities(activities) do
|
||||||
|
activities
|
||||||
|
|> Enum.map(fn
|
||||||
|
%{data: %{"type" => "Create"}} = activity ->
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end)
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
|> Activity.create_by_object_ap_id_with_object()
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.reduce(%{}, fn activity, acc ->
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
if object, do: Map.put(acc, object.data["id"], activity), else: acc
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
# DEPRECATED This field seems to be a left-over from the StatusNet era.
|
# DEPRECATED This field seems to be a left-over from the StatusNet era.
|
||||||
# If your application uses `pleroma.conversation_id`: this field is deprecated.
|
# If your application uses `pleroma.conversation_id`: this field is deprecated.
|
||||||
# It is currently stubbed instead by doing a CRC32 of the context, and
|
# It is currently stubbed instead by doing a CRC32 of the context, and
|
||||||
|
@ -97,6 +118,7 @@ def render("index.json", opts) do
|
||||||
# length(activities_with_links) * timeout
|
# length(activities_with_links) * timeout
|
||||||
fetch_rich_media_for_activities(activities)
|
fetch_rich_media_for_activities(activities)
|
||||||
replied_to_activities = get_replied_to_activities(activities)
|
replied_to_activities = get_replied_to_activities(activities)
|
||||||
|
quoted_activities = get_quoted_activities(activities)
|
||||||
|
|
||||||
parent_activities =
|
parent_activities =
|
||||||
activities
|
activities
|
||||||
|
@ -129,6 +151,7 @@ def render("index.json", opts) do
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Map.put(:replied_to_activities, replied_to_activities)
|
|> Map.put(:replied_to_activities, replied_to_activities)
|
||||||
|
|> Map.put(:quoted_activities, quoted_activities)
|
||||||
|> Map.put(:parent_activities, parent_activities)
|
|> Map.put(:parent_activities, parent_activities)
|
||||||
|> Map.put(:relationships, relationships_opt)
|
|> Map.put(:relationships, relationships_opt)
|
||||||
|
|
||||||
|
@ -277,7 +300,6 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
end
|
end
|
||||||
|
|
||||||
reply_to = get_reply_to(activity, opts)
|
reply_to = get_reply_to(activity, opts)
|
||||||
|
|
||||||
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
||||||
|
|
||||||
history_len =
|
history_len =
|
||||||
|
@ -290,6 +312,22 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
# Here the implicit index of the current content is 0
|
# Here the implicit index of the current content is 0
|
||||||
chrono_order = history_len - 1
|
chrono_order = history_len - 1
|
||||||
|
|
||||||
|
quote_activity = get_quote(activity, opts)
|
||||||
|
|
||||||
|
quote_id =
|
||||||
|
case quote_activity do
|
||||||
|
%Activity{id: id} -> id
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
quote_post =
|
||||||
|
if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
|
||||||
|
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
|
||||||
|
render("show.json", quote_rendering_opts)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
content =
|
content =
|
||||||
object
|
object
|
||||||
|> render_content()
|
|> render_content()
|
||||||
|
@ -398,6 +436,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
conversation_id: get_context_id(activity),
|
conversation_id: get_context_id(activity),
|
||||||
context: object.data["context"],
|
context: object.data["context"],
|
||||||
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
|
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
|
||||||
|
quote: quote_post,
|
||||||
|
quote_id: quote_id,
|
||||||
|
quote_url: object.data["quoteUrl"],
|
||||||
|
quote_visible: visible_for_user?(quote_activity, opts[:for]),
|
||||||
content: %{"text/plain" => content_plaintext},
|
content: %{"text/plain" => content_plaintext},
|
||||||
spoiler_text: %{"text/plain" => summary},
|
spoiler_text: %{"text/plain" => summary},
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
|
@ -633,6 +675,25 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_quote(activity, %{quoted_activities: quoted_activities}) do
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
|
with nil <- quoted_activities[object.data["quoteUrl"]] do
|
||||||
|
# For when a quote post is inside an Announce
|
||||||
|
Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_quote(%{data: %{"object" => _object}} = activity, _) do
|
||||||
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
|
if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
|
||||||
|
Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
|
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
|
||||||
url = object.data["url"] || object.data["id"]
|
url = object.data["url"] || object.data["id"]
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
|
alias Pleroma.Web.StreamerView
|
||||||
|
|
||||||
@behaviour :cowboy_websocket
|
@behaviour :cowboy_websocket
|
||||||
|
|
||||||
|
@ -32,8 +33,15 @@ def init(%{qs: qs} = req, state) do
|
||||||
req
|
req
|
||||||
end
|
end
|
||||||
|
|
||||||
|
topics =
|
||||||
|
if topic do
|
||||||
|
[topic]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
{:cowboy_websocket, req,
|
{:cowboy_websocket, req,
|
||||||
%{user: user, topic: topic, oauth_token: oauth_token, count: 0, timer: nil},
|
%{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
|
||||||
%{idle_timeout: @timeout}}
|
%{idle_timeout: @timeout}}
|
||||||
else
|
else
|
||||||
{:error, :bad_topic} ->
|
{:error, :bad_topic} ->
|
||||||
|
@ -50,10 +58,10 @@ def init(%{qs: qs} = req, state) do
|
||||||
|
|
||||||
def websocket_init(state) do
|
def websocket_init(state) do
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}"
|
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
|
||||||
)
|
)
|
||||||
|
|
||||||
Streamer.add_socket(state.topic, state.oauth_token)
|
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
|
||||||
{:ok, %{state | timer: timer()}}
|
{:ok, %{state | timer: timer()}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,16 +74,26 @@ def websocket_handle(:pong, state) do
|
||||||
# We only receive pings for now
|
# We only receive pings for now
|
||||||
def websocket_handle(:ping, state), do: {:ok, state}
|
def websocket_handle(:ping, state), do: {:ok, state}
|
||||||
|
|
||||||
|
def websocket_handle({:text, text}, state) do
|
||||||
|
with {:ok, %{} = event} <- Jason.decode(text) do
|
||||||
|
handle_client_event(event, state)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}")
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def websocket_handle(frame, state) do
|
def websocket_handle(frame, state) do
|
||||||
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
|
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def websocket_info({:render_with_user, view, template, item}, state) do
|
def websocket_info({:render_with_user, view, template, item, topic}, state) do
|
||||||
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
|
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
|
||||||
|
|
||||||
unless Streamer.filtered_by_user?(user, item) do
|
unless Streamer.filtered_by_user?(user, item) do
|
||||||
websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
|
websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
|
||||||
else
|
else
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
@ -109,10 +127,10 @@ def terminate(_reason, _req, []), do: :ok
|
||||||
|
|
||||||
def terminate(reason, _req, state) do
|
def terminate(reason, _req, state) do
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
Streamer.remove_socket(state.topic)
|
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -137,4 +155,103 @@ defp authenticate_request(access_token, sec_websocket) do
|
||||||
defp timer do
|
defp timer do
|
||||||
Process.send_after(self(), :tick, @tick)
|
Process.send_after(self(), :tick, @tick)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
|
||||||
|
with {_, {:ok, topic}} <-
|
||||||
|
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
|
||||||
|
{_, false} <- {:subscribed, topic in state.topics} do
|
||||||
|
Streamer.add_socket(topic, state.oauth_token)
|
||||||
|
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
|
||||||
|
], %{state | topics: [topic | state.topics]}}
|
||||||
|
else
|
||||||
|
{:subscribed, true} ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
|
||||||
|
], state}
|
||||||
|
|
||||||
|
{:topic, {:error, error}} ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{
|
||||||
|
type: "subscribe",
|
||||||
|
result: "error",
|
||||||
|
error: error
|
||||||
|
})}
|
||||||
|
], state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params, state) do
|
||||||
|
with {_, {:ok, topic}} <-
|
||||||
|
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
|
||||||
|
{_, true} <- {:subscribed, topic in state.topics} do
|
||||||
|
Streamer.remove_socket(topic)
|
||||||
|
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
|
||||||
|
], %{state | topics: List.delete(state.topics, topic)}}
|
||||||
|
else
|
||||||
|
{:subscribed, false} ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
|
||||||
|
], state}
|
||||||
|
|
||||||
|
{:topic, {:error, error}} ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{
|
||||||
|
type: "unsubscribe",
|
||||||
|
result: "error",
|
||||||
|
error: error
|
||||||
|
})}
|
||||||
|
], state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_client_event(
|
||||||
|
%{"type" => "pleroma:authenticate", "token" => access_token} = _params,
|
||||||
|
state
|
||||||
|
) do
|
||||||
|
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
|
||||||
|
{:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{
|
||||||
|
type: "pleroma:authenticate",
|
||||||
|
result: "success"
|
||||||
|
})}
|
||||||
|
], %{state | user: user, oauth_token: oauth_token}}
|
||||||
|
else
|
||||||
|
{:auth, _, _} ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{
|
||||||
|
type: "pleroma:authenticate",
|
||||||
|
result: "error",
|
||||||
|
error: :already_authenticated
|
||||||
|
})}
|
||||||
|
], state}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{[
|
||||||
|
{:text,
|
||||||
|
StreamerView.render("pleroma_respond.json", %{
|
||||||
|
type: "pleroma:authenticate",
|
||||||
|
result: "error",
|
||||||
|
error: :unauthorized
|
||||||
|
})}
|
||||||
|
], state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_client_event(params, state) do
|
||||||
|
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
|
||||||
|
{[], state}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,18 +93,26 @@ defp csp_string do
|
||||||
|
|
||||||
img_src = "img-src 'self' data: blob:"
|
img_src = "img-src 'self' data: blob:"
|
||||||
media_src = "media-src 'self'"
|
media_src = "media-src 'self'"
|
||||||
|
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||||
|
|
||||||
# Strict multimedia CSP enforcement only when MediaProxy is enabled
|
# Strict multimedia CSP enforcement only when MediaProxy is enabled
|
||||||
{img_src, media_src} =
|
{img_src, media_src, connect_src} =
|
||||||
if Config.get([:media_proxy, :enabled]) &&
|
if Config.get([:media_proxy, :enabled]) &&
|
||||||
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||||
sources = build_csp_multimedia_source_list()
|
sources = build_csp_multimedia_source_list()
|
||||||
{[img_src, sources], [media_src, sources]}
|
|
||||||
else
|
|
||||||
{[img_src, " https:"], [media_src, " https:"]}
|
|
||||||
end
|
|
||||||
|
|
||||||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
{
|
||||||
|
[img_src, sources],
|
||||||
|
[media_src, sources],
|
||||||
|
[connect_src, sources]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
[img_src, " https:"],
|
||||||
|
[media_src, " https:"],
|
||||||
|
[connect_src, " https:"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
connect_src =
|
connect_src =
|
||||||
if Config.get(:env) == :dev do
|
if Config.get(:env) == :dev do
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.Streamer do
|
defmodule Pleroma.Web.Streamer do
|
||||||
require Logger
|
require Logger
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Chat.MessageReference
|
alias Pleroma.Chat.MessageReference
|
||||||
|
@ -24,7 +25,7 @@ defmodule Pleroma.Web.Streamer do
|
||||||
|
|
||||||
def registry, do: @registry
|
def registry, do: @registry
|
||||||
|
|
||||||
@public_streams ["public", "public:local", "public:media", "public:local:media"]
|
@public_streams Pleroma.Constants.public_streams()
|
||||||
@local_streams ["public:local", "public:local:media"]
|
@local_streams ["public:local", "public:local:media"]
|
||||||
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
|
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
|
||||||
|
|
||||||
|
@ -59,10 +60,14 @@ defp can_access_stream(user, oauth_token, kind) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Expand and authorizes a stream"
|
@doc "Expand and authorizes a stream"
|
||||||
@spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
|
@spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, Map.t()) ::
|
||||||
{:ok, topic :: String.t()} | {:error, :bad_topic}
|
{:ok, topic :: String.t() | nil} | {:error, :bad_topic}
|
||||||
def get_topic(stream, user, oauth_token, params \\ %{})
|
def get_topic(stream, user, oauth_token, params \\ %{})
|
||||||
|
|
||||||
|
def get_topic(nil = _stream, _user, _oauth_token, _params) do
|
||||||
|
{:ok, nil}
|
||||||
|
end
|
||||||
|
|
||||||
# Allow all public steams if the instance allows unauthenticated access.
|
# Allow all public steams if the instance allows unauthenticated access.
|
||||||
# Otherwise, only allow users with valid oauth tokens.
|
# Otherwise, only allow users with valid oauth tokens.
|
||||||
def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
|
def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
|
||||||
|
@ -219,8 +224,8 @@ defp do_stream("direct", item) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_stream("follow_relationship", item) do
|
defp do_stream("follow_relationship", item) do
|
||||||
text = StreamerView.render("follow_relationships_update.json", item)
|
|
||||||
user_topic = "user:#{item.follower.id}"
|
user_topic = "user:#{item.follower.id}"
|
||||||
|
text = StreamerView.render("follow_relationships_update.json", item, user_topic)
|
||||||
|
|
||||||
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
|
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
|
||||||
|
|
||||||
|
@ -266,9 +271,11 @@ defp do_stream("list", item) do
|
||||||
|
|
||||||
defp do_stream(topic, %Notification{} = item)
|
defp do_stream(topic, %Notification{} = item)
|
||||||
when topic in ["user", "user:notification"] do
|
when topic in ["user", "user:notification"] do
|
||||||
Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list ->
|
user_topic = "#{topic}:#{item.user_id}"
|
||||||
|
|
||||||
|
Registry.dispatch(@registry, user_topic, fn list ->
|
||||||
Enum.each(list, fn {pid, _auth} ->
|
Enum.each(list, fn {pid, _auth} ->
|
||||||
send(pid, {:render_with_user, StreamerView, "notification.json", item})
|
send(pid, {:render_with_user, StreamerView, "notification.json", item, user_topic})
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -277,7 +284,7 @@ defp do_stream(topic, {user, %MessageReference{} = cm_ref})
|
||||||
when topic in ["user", "user:pleroma_chat"] do
|
when topic in ["user", "user:pleroma_chat"] do
|
||||||
topic = "#{topic}:#{user.id}"
|
topic = "#{topic}:#{user.id}"
|
||||||
|
|
||||||
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
|
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic)
|
||||||
|
|
||||||
Registry.dispatch(@registry, topic, fn list ->
|
Registry.dispatch(@registry, topic, fn list ->
|
||||||
Enum.each(list, fn {pid, _auth} ->
|
Enum.each(list, fn {pid, _auth} ->
|
||||||
|
@ -305,7 +312,7 @@ defp do_stream(topic, item) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp push_to_socket(topic, %Participation{} = participation) do
|
defp push_to_socket(topic, %Participation{} = participation) do
|
||||||
rendered = StreamerView.render("conversation.json", participation)
|
rendered = StreamerView.render("conversation.json", participation, topic)
|
||||||
|
|
||||||
Registry.dispatch(@registry, topic, fn list ->
|
Registry.dispatch(@registry, topic, fn list ->
|
||||||
Enum.each(list, fn {pid, _} ->
|
Enum.each(list, fn {pid, _} ->
|
||||||
|
@ -333,12 +340,15 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
|
||||||
Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
|
Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
|
||||||
|> Map.put(:object, item.object)
|
|> Map.put(:object, item.object)
|
||||||
|
|
||||||
anon_render = StreamerView.render("status_update.json", create_activity)
|
anon_render = StreamerView.render("status_update.json", create_activity, topic)
|
||||||
|
|
||||||
Registry.dispatch(@registry, topic, fn list ->
|
Registry.dispatch(@registry, topic, fn list ->
|
||||||
Enum.each(list, fn {pid, auth?} ->
|
Enum.each(list, fn {pid, auth?} ->
|
||||||
if auth? do
|
if auth? do
|
||||||
send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity})
|
send(
|
||||||
|
pid,
|
||||||
|
{:render_with_user, StreamerView, "status_update.json", create_activity, topic}
|
||||||
|
)
|
||||||
else
|
else
|
||||||
send(pid, {:text, anon_render})
|
send(pid, {:text, anon_render})
|
||||||
end
|
end
|
||||||
|
@ -347,12 +357,12 @@ defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp push_to_socket(topic, item) do
|
defp push_to_socket(topic, item) do
|
||||||
anon_render = StreamerView.render("update.json", item)
|
anon_render = StreamerView.render("update.json", item, topic)
|
||||||
|
|
||||||
Registry.dispatch(@registry, topic, fn list ->
|
Registry.dispatch(@registry, topic, fn list ->
|
||||||
Enum.each(list, fn {pid, auth?} ->
|
Enum.each(list, fn {pid, auth?} ->
|
||||||
if auth? do
|
if auth? do
|
||||||
send(pid, {:render_with_user, StreamerView, "update.json", item})
|
send(pid, {:render_with_user, StreamerView, "update.json", item, topic})
|
||||||
else
|
else
|
||||||
send(pid, {:text, anon_render})
|
send(pid, {:text, anon_render})
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,8 +11,11 @@ defmodule Pleroma.Web.StreamerView do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||||
|
|
||||||
def render("update.json", %Activity{} = activity, %User{} = user) do
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
def render("update.json", %Activity{} = activity, %User{} = user, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "update",
|
event: "update",
|
||||||
payload:
|
payload:
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
@ -25,8 +28,9 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("status_update.json", %Activity{} = activity, %User{} = user) do
|
def render("status_update.json", %Activity{} = activity, %User{} = user, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "status.update",
|
event: "status.update",
|
||||||
payload:
|
payload:
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
@ -39,8 +43,9 @@ def render("status_update.json", %Activity{} = activity, %User{} = user) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("notification.json", %Notification{} = notify, %User{} = user) do
|
def render("notification.json", %Notification{} = notify, %User{} = user, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "notification",
|
event: "notification",
|
||||||
payload:
|
payload:
|
||||||
NotificationView.render(
|
NotificationView.render(
|
||||||
|
@ -52,8 +57,9 @@ def render("notification.json", %Notification{} = notify, %User{} = user) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("update.json", %Activity{} = activity) do
|
def render("update.json", %Activity{} = activity, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "update",
|
event: "update",
|
||||||
payload:
|
payload:
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
@ -65,8 +71,9 @@ def render("update.json", %Activity{} = activity) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("status_update.json", %Activity{} = activity) do
|
def render("status_update.json", %Activity{} = activity, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "status.update",
|
event: "status.update",
|
||||||
payload:
|
payload:
|
||||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||||
|
@ -78,7 +85,7 @@ def render("status_update.json", %Activity{} = activity) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
def render("chat_update.json", %{chat_message_reference: cm_ref}, topic) do
|
||||||
# Explicitly giving the cmr for the object here, so we don't accidentally
|
# Explicitly giving the cmr for the object here, so we don't accidentally
|
||||||
# send a later 'last_message' that was inserted between inserting this and
|
# send a later 'last_message' that was inserted between inserting this and
|
||||||
# streaming it out
|
# streaming it out
|
||||||
|
@ -93,6 +100,7 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
||||||
)
|
)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "pleroma:chat_update",
|
event: "pleroma:chat_update",
|
||||||
payload:
|
payload:
|
||||||
representation
|
representation
|
||||||
|
@ -101,8 +109,9 @@ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("follow_relationships_update.json", item) do
|
def render("follow_relationships_update.json", item, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "pleroma:follow_relationships_update",
|
event: "pleroma:follow_relationships_update",
|
||||||
payload:
|
payload:
|
||||||
%{
|
%{
|
||||||
|
@ -123,8 +132,9 @@ def render("follow_relationships_update.json", item) do
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("conversation.json", %Participation{} = participation) do
|
def render("conversation.json", %Participation{} = participation, topic) do
|
||||||
%{
|
%{
|
||||||
|
stream: render("stream.json", %{topic: topic}),
|
||||||
event: "conversation",
|
event: "conversation",
|
||||||
payload:
|
payload:
|
||||||
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
|
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
|
||||||
|
@ -135,4 +145,39 @@ def render("conversation.json", %Participation{} = participation) do
|
||||||
}
|
}
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("pleroma_respond.json", %{type: type, result: result} = params) do
|
||||||
|
%{
|
||||||
|
event: "pleroma:respond",
|
||||||
|
payload:
|
||||||
|
%{
|
||||||
|
result: result,
|
||||||
|
type: type
|
||||||
|
}
|
||||||
|
|> Map.merge(maybe_error(params))
|
||||||
|
|> Jason.encode!()
|
||||||
|
}
|
||||||
|
|> Jason.encode!()
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("stream.json", %{topic: "user:pleroma_chat:" <> _}), do: ["user:pleroma_chat"]
|
||||||
|
def render("stream.json", %{topic: "user:notification:" <> _}), do: ["user:notification"]
|
||||||
|
def render("stream.json", %{topic: "user:" <> _}), do: ["user"]
|
||||||
|
def render("stream.json", %{topic: "direct:" <> _}), do: ["direct"]
|
||||||
|
def render("stream.json", %{topic: "list:" <> id}), do: ["list", id]
|
||||||
|
def render("stream.json", %{topic: "hashtag:" <> tag}), do: ["hashtag", tag]
|
||||||
|
|
||||||
|
def render("stream.json", %{topic: "public:remote:media:" <> instance}),
|
||||||
|
do: ["public:remote:media", instance]
|
||||||
|
|
||||||
|
def render("stream.json", %{topic: "public:remote:" <> instance}),
|
||||||
|
do: ["public:remote", instance]
|
||||||
|
|
||||||
|
def render("stream.json", %{topic: stream}) when stream in Pleroma.Constants.public_streams(),
|
||||||
|
do: [stream]
|
||||||
|
|
||||||
|
defp maybe_error(%{error: :bad_topic}), do: %{error: "bad_topic"}
|
||||||
|
defp maybe_error(%{error: :unauthorized}), do: %{error: "unauthorized"}
|
||||||
|
defp maybe_error(%{error: :already_authenticated}), do: %{error: "already_authenticated"}
|
||||||
|
defp maybe_error(_), do: %{}
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
|
||||||
The worker to send digest emails.
|
The worker to send digest emails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker, queue: "digest_emails"
|
use Oban.Worker, queue: "mailer"
|
||||||
|
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.Emails
|
alias Pleroma.Emails
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
use Pleroma.Workers.WorkerHelper, queue: "new_users_digest"
|
use Pleroma.Workers.WorkerHelper, queue: "mailer"
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(_job) do
|
def perform(_job) do
|
||||||
|
|
6
mix.exs
6
mix.exs
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pleroma,
|
app: :pleroma,
|
||||||
version: version("2.5.54"),
|
version: version("2.6.50"),
|
||||||
elixir: "~> 1.11",
|
elixir: "~> 1.11",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: [:phoenix] ++ Mix.compilers(),
|
compilers: [:phoenix] ++ Mix.compilers(),
|
||||||
|
@ -176,7 +176,6 @@ defp deps do
|
||||||
{:prometheus_ecto, "~> 1.4"},
|
{:prometheus_ecto, "~> 1.4"},
|
||||||
{:recon, "~> 2.5"},
|
{:recon, "~> 2.5"},
|
||||||
{:joken, "~> 2.0"},
|
{:joken, "~> 2.0"},
|
||||||
{:benchee, "~> 1.0"},
|
|
||||||
{:pot, "~> 1.0"},
|
{:pot, "~> 1.0"},
|
||||||
{:ex_const, "~> 0.2"},
|
{:ex_const, "~> 0.2"},
|
||||||
{:plug_static_index_html, "~> 1.0.0"},
|
{:plug_static_index_html, "~> 1.0.0"},
|
||||||
|
@ -202,7 +201,8 @@ defp deps do
|
||||||
{:covertool, "~> 2.0", only: :test},
|
{:covertool, "~> 2.0", only: :test},
|
||||||
{:hackney, "~> 1.18.0", override: true},
|
{:hackney, "~> 1.18.0", override: true},
|
||||||
{:mox, "~> 1.0", only: :test},
|
{:mox, "~> 1.0", only: :test},
|
||||||
{:websockex, "~> 0.4.3", only: :test}
|
{:websockex, "~> 0.4.3", only: :test},
|
||||||
|
{:benchee, "~> 1.0", only: :benchmark}
|
||||||
] ++ oauth_deps()
|
] ++ oauth_deps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.ConsolidateEmailQueues do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
execute(
|
||||||
|
"UPDATE oban_jobs SET queue = 'mailer' WHERE queue in ('digest_emails', 'new_users_digest')"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -38,6 +38,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
|
||||||
Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"])
|
Meta.allow_tag_with_these_attributes(:abbr, ["title", "lang"])
|
||||||
|
|
||||||
Meta.allow_tag_with_these_attributes(:b, ["lang"])
|
Meta.allow_tag_with_these_attributes(:b, ["lang"])
|
||||||
|
Meta.allow_tag_with_these_attributes(:bdi, [])
|
||||||
Meta.allow_tag_with_these_attributes(:blockquote, ["lang"])
|
Meta.allow_tag_with_these_attributes(:blockquote, ["lang"])
|
||||||
Meta.allow_tag_with_these_attributes(:br, ["lang"])
|
Meta.allow_tag_with_these_attributes(:br, ["lang"])
|
||||||
Meta.allow_tag_with_these_attributes(:code, ["lang"])
|
Meta.allow_tag_with_these_attributes(:code, ["lang"])
|
||||||
|
@ -60,7 +61,12 @@ defmodule Pleroma.HTML.Scrubber.Default do
|
||||||
Meta.allow_tag_with_these_attributes(:u, ["lang"])
|
Meta.allow_tag_with_these_attributes(:u, ["lang"])
|
||||||
Meta.allow_tag_with_these_attributes(:ul, ["lang"])
|
Meta.allow_tag_with_these_attributes(:ul, ["lang"])
|
||||||
|
|
||||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "recipients-inline"])
|
Meta.allow_tag_with_this_attribute_values(:span, "class", [
|
||||||
|
"h-card",
|
||||||
|
"recipients-inline",
|
||||||
|
"quote-inline"
|
||||||
|
])
|
||||||
|
|
||||||
Meta.allow_tag_with_these_attributes(:span, ["lang"])
|
Meta.allow_tag_with_these_attributes(:span, ["lang"])
|
||||||
|
|
||||||
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
|
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><script defer=defer src=/static/js/2724.e4840c73281069ba54ab.js></script><script defer=defer src=/static/js/app.8d2126d35dba9482db51.js></script><link href=/static/css/app.48e52505beba5b9ab69b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><div id=modal></div><div id=popovers></body></html>
|
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><script defer=defer src=/static/js/3733.7060d1e6bca813125a0c.js></script><script defer=defer src=/static/js/app.7c4b412b26221a7c8572.js></script><link href=/static/css/app.c18a2c80794a1b699a61.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><div id=modal></div><div id=popovers></body></html>
|
|
@ -17,6 +17,7 @@
|
||||||
"ostatus": "http://ostatus.org#",
|
"ostatus": "http://ostatus.org#",
|
||||||
"schema": "http://schema.org#",
|
"schema": "http://schema.org#",
|
||||||
"toot": "http://joinmastodon.org/ns#",
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"fedibird": "http://fedibird.com/ns#",
|
||||||
"value": "schema:value",
|
"value": "schema:value",
|
||||||
"sensitive": "as:sensitive",
|
"sensitive": "as:sensitive",
|
||||||
"litepub": "http://litepub.social/ns#",
|
"litepub": "http://litepub.social/ns#",
|
||||||
|
@ -26,6 +27,8 @@
|
||||||
"@id": "litepub:listMessage",
|
"@id": "litepub:listMessage",
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
},
|
},
|
||||||
|
"quoteUrl": "as:quoteUrl",
|
||||||
|
"quoteUri": "fedibird:quoteUri",
|
||||||
"oauthRegistrationEndpoint": {
|
"oauthRegistrationEndpoint": {
|
||||||
"@id": "litepub:oauthRegistrationEndpoint",
|
"@id": "litepub:oauthRegistrationEndpoint",
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
|
|
Binary file not shown.
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"static/css/5948.06d2a0d84620cba6a4fb.css","mappings":"AACA,uBAGE,mBAFA,aACA,YAEA,uBAEA,4BACE,YACA,iBCPJ,gBACE,gBAEA,2DAEE,qBACA,iBAEA,iEACE,mBAGF,mFACE,gBAIJ,sCAOE,YADA,eALA,gBACA,qBAEA,wBADA,uCAEA,YAEA,CAEA,yBATF,sCAWI,YADA,eACA,EAGF,kDACE,YACA,kBAEA,uDACE,eACA,eACA,cAKN,iCACE,aAEA,mCACE,kBAGF,gDACE,aACA,YAKF,2CASE,8CAEA,yBAXF,2CAgBI","sources":["webpack://pleroma_fe/./src/components/async_component_error/async_component_error.vue","webpack://pleroma_fe/./src/components/settings_modal/settings_modal.scss"],"sourcesContent":["\n.async-component-error {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n\n .btn {\n margin: 0.5em;\n padding: 0.5em 2em;\n }\n}\n","@import \"src/variables\";\n\n.settings-modal {\n overflow: hidden;\n\n .setting-list,\n .option-list {\n list-style-type: none;\n padding-left: 2em;\n\n li {\n margin-bottom: 0.5em;\n }\n\n .suboptions {\n margin-top: 0.3em;\n }\n }\n\n .settings-modal-panel {\n overflow: hidden;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 300ms;\n width: 1000px;\n max-width: 90vw;\n height: 90vh;\n\n @media all and (max-width: 800px) {\n max-width: 100vw;\n height: 100%;\n }\n\n >.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n min-width: 10em;\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""}
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"static/css/7586.0d43f70bc6240422f179.css","mappings":"AACA,uBAGE,mBAFA,aACA,YAEA,uBAEA,4BACE,YACA,iBCPJ,gBACE,gBAEA,2DAEE,qBACA,iBAEA,iEACE,mBAGF,mFACE,gBAIJ,qCAGE,cADA,kBADA,eAEA,CAGF,sCAOE,YADA,eALA,gBACA,qBAEA,wBADA,uCAEA,YAEA,CAEA,yBATF,sCAWI,YADA,eACA,EAGF,kDACE,YACA,kBAEA,uDACE,eAGF,6EACE,cAKN,iCACE,aACA,eACA,cAEA,mCACE,kBAGF,gDACE,aACA,YAKF,2CASE,8CAEA,yBAXF,2CAgBI","sources":["webpack://pleroma_fe/./src/components/async_component_error/async_component_error.vue","webpack://pleroma_fe/./src/components/settings_modal/settings_modal.scss"],"sourcesContent":["\n.async-component-error {\n display: flex;\n height: 100%;\n align-items: center;\n justify-content: center;\n\n .btn {\n margin: 0.5em;\n padding: 0.5em 2em;\n }\n}\n","@import \"src/variables\";\n\n.settings-modal {\n overflow: hidden;\n\n .setting-list,\n .option-list {\n list-style-type: none;\n padding-left: 2em;\n\n li {\n margin-bottom: 0.5em;\n }\n\n .suboptions {\n margin-top: 0.3em;\n }\n }\n\n .setting-description {\n margin-top: 0.2em;\n margin-bottom: 2em;\n font-size: 70%;\n }\n\n .settings-modal-panel {\n overflow: hidden;\n transition: transform;\n transition-timing-function: ease-in-out;\n transition-duration: 300ms;\n width: 1000px;\n max-width: 90vw;\n height: 90vh;\n\n @media all and (max-width: 800px) {\n max-width: 100vw;\n height: 100%;\n }\n\n >.panel-body {\n height: 100%;\n overflow-y: hidden;\n\n .btn {\n min-height: 2em;\n }\n\n .btn:not(.dropdown-button) {\n padding: 0 2em;\n }\n }\n }\n\n .settings-footer {\n display: flex;\n flex-wrap: wrap;\n line-height: 2;\n\n >* {\n margin-right: 0.5em;\n }\n\n .extra-content {\n display: flex;\n flex-grow: 1;\n }\n }\n\n &.peek {\n .settings-modal-panel {\n /* Explanation:\n * Modal is positioned vertically centered.\n * 100vh - 100% = Distance between modal's top+bottom boundaries and screen\n * (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen\n * + 100% - we move modal completely off-screen, it's top boundary touches\n * bottom of the screen\n * - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible\n */\n transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));\n\n @media all and (max-width: 800px) {\n /* For mobile, the modal takes 100% of the available screen.\n This ensures the minimized modal is always 50px above the browser bottom\n bar regardless of whether or not it is visible.\n */\n transform: translateY(calc(100% - 50px));\n }\n }\n }\n}\n"],"names":[],"sourceRoot":""}
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"static/css/9801.cfe503d4c949ae0c3813.css","mappings":"AACA,mBACE,qBACA,kBAGF,kBACE,gBACA,eACA,kBCRF,yBACE,qBACA,kBAGF,wBACE,gBACA,eACA,kBCRF,cACE,qBACA,kBAEA,8BACE,iBAIJ,eACE,gBACA,eACA,kBCXA,+BACE,cAEA,YACA,mBAFA,UAEA,CAGF,qCAEE,aACA,sBAFA,gBAGA,WAGF,6BACE,mBAEA,uEAEE,WCpBJ,2BACE,UAGF,kBAEE,iBAGA,eADA,kBAHA,uBAEA,kBAEA,CCRJ,uBACE,YAEA,qCACE,0CACA,qBACA,qBAEA,oFAEE,cACA,mBAEA,0GACE,gBAIJ,sDACE,aAEA,mEACE,SACA,kBAIJ,gDACE,mBAEA,kBADA,gBACA,CAGF,4CACE,eAGF,8CAGE,aADA,eADA,UAEA,CAGF,wGAEE,sBACA,SCnCW","sources":["webpack://pleroma_fe/./src/components/settings_modal/helpers/modified_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/profile_setting_indicator.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/draft_buttons.vue","webpack://pleroma_fe/./src/components/settings_modal/helpers/attachment_setting.vue","webpack://pleroma_fe/./src/components/settings_modal/admin_tabs/frontends_tab.scss","webpack://pleroma_fe/./src/components/settings_modal/settings_modal_admin_content.scss","webpack://pleroma_fe/./src/_variables.scss"],"sourcesContent":["\n.ModifiedIndicator {\n display: inline-block;\n position: relative;\n}\n\n.modified-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.ProfileSettingIndicator {\n display: inline-block;\n position: relative;\n}\n\n.profilesetting-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.DraftButtons {\n display: inline-block;\n position: relative;\n\n .button-default {\n margin-left: 0.5em;\n }\n}\n\n.draft-tooltip {\n margin: 0.5em 1em;\n min-width: 10em;\n text-align: center;\n}\n","\n.AttachmentSetting {\n .attachment {\n display: block;\n width: 100%;\n height: 15em;\n margin-bottom: 0.5em;\n }\n\n .attachment-input {\n margin-left: 1em;\n display: flex;\n flex-direction: column;\n width: 20em;\n }\n\n .controls {\n margin-bottom: 0.5em;\n\n input,\n button {\n width: 100%;\n }\n }\n}\n",".frontends-tab {\n .cards-list {\n padding: 0;\n }\n\n dd {\n text-overflow: ellipsis;\n word-wrap: nowrap;\n white-space: nowrap;\n overflow-x: hidden;\n max-width: 10em;\n }\n}\n","@import \"src/variables\";\n\n.settings_tab-switcher {\n height: 100%;\n\n .setting-item {\n border-bottom: 2px solid var(--fg, $fallback--fg);\n margin: 1em 1em 1.4em;\n padding-bottom: 1.4em;\n\n > div,\n > label {\n display: block;\n margin-bottom: 0.5em;\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n .select-multiple {\n display: flex;\n\n .option-list {\n margin: 0;\n padding-left: 0.5em;\n }\n }\n\n &:last-child {\n border-bottom: none;\n padding-bottom: 0;\n margin-bottom: 1em;\n }\n\n select {\n min-width: 10em;\n }\n\n textarea {\n width: 100%;\n max-width: 100%;\n height: 100px;\n }\n\n .unavailable,\n .unavailable svg {\n color: var(--cRed, $fallback--cRed);\n color: $fallback--cRed;\n }\n }\n}\n","$main-color: #f58d2c;\n$main-background: white;\n$darkened-background: whitesmoke;\n\n$fallback--bg: #121a24;\n$fallback--fg: #182230;\n$fallback--faint: rgb(185 185 186 / 50%);\n$fallback--text: #b9b9ba;\n$fallback--link: #d8a070;\n$fallback--icon: #666;\n$fallback--lightBg: rgb(21 30 42);\n$fallback--lightText: #b9b9ba;\n$fallback--border: #222;\n$fallback--cRed: #f00;\n$fallback--cBlue: #0095ff;\n$fallback--cGreen: #0fa00f;\n$fallback--cOrange: orange;\n\n$fallback--alertError: rgb(211 16 20 / 50%);\n$fallback--alertWarning: rgb(111 111 20 / 50%);\n\n$fallback--panelRadius: 10px;\n$fallback--checkboxRadius: 2px;\n$fallback--btnRadius: 4px;\n$fallback--inputRadius: 4px;\n$fallback--tooltipRadius: 5px;\n$fallback--avatarRadius: 4px;\n$fallback--avatarAltRadius: 10px;\n$fallback--attachmentRadius: 10px;\n$fallback--chatMessageRadius: 10px;\n\n$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%),\n 0 1px 0 0 rgb(255 255 255 / 20%) inset,\n 0 -1px 0 0 rgb(0 0 0 / 20%) inset;\n\n$status-margin: 0.75em;\n"],"names":[],"sourceRoot":""}
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue