Merge branch 'develop' into issue/1276

This commit is contained in:
Maksim Pechnikov 2020-03-29 06:57:34 +03:00
commit dfd2c74184
259 changed files with 4015 additions and 1724 deletions

View file

@ -62,19 +62,21 @@ unit-testing:
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --preload-modules - mix coveralls --preload-modules
federated-testing: # Removed to fix CI issue. In this early state it wasn't adding much value anyway.
stage: test # TODO Fix and reinstate federated testing
cache: *testing_cache_policy # federated-testing:
services: # stage: test
- name: minibikini/postgres-with-rum:12 # cache: *testing_cache_policy
alias: postgres # services:
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] # - name: minibikini/postgres-with-rum:12
script: # alias: postgres
- mix deps.get # command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- mix ecto.create # script:
- mix ecto.migrate # - mix deps.get
- epmd -daemon # - mix ecto.create
- mix test --trace --only federated # - mix ecto.migrate
# - epmd -daemon
# - mix test --trace --only federated
unit-testing-rum: unit-testing-rum:
stage: test stage: test

View file

@ -0,0 +1,20 @@
<!--
### Precheck
* For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels).
* Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it.
* Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository.
-->
### Environment
* Installation type:
- [ ] OTP
- [ ] From source
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system:
* PostgreSQL version (`postgres -V`):
### Bug description

View file

@ -0,0 +1,5 @@
### Release checklist
* [ ] Bump version in `mix.exs`
* [ ] Compile a changelog
* [ ] Create an MR with an announcement to pleroma.social
* [ ] Tag the release

View file

@ -3,7 +3,22 @@ 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] ## [unreleased]
### Changed
- **Breaking:** BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
### Removed
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
<details>
<summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
</details>
## [2.0.0] - 2019-03-08
### Security ### Security
- Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request. - Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
@ -38,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled) - Rate limiter is now disabled for localhost/socket (unless remoteip plug is enabled)
- Logger: default log level changed from `warn` to `info`. - Logger: default log level changed from `warn` to `info`.
- Config mix task `migrate_to_db` truncates `config` table before migrating the config file. - Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
- Allow account registration without an email
- Default to `prepare: :unnamed` in the database configuration. - Default to `prepare: :unnamed` in the database configuration.
- Instance stats are now loaded on startup instead of being empty until next hourly job. - Instance stats are now loaded on startup instead of being empty until next hourly job.
<details> <details>
@ -63,6 +79,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
- Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default. - Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
</details> </details>
### Added ### Added
@ -151,6 +168,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Marking a conversation as read (`POST /api/v1/conversations/:id/read`) now no longer brings it to the top in the user's direct conversation list - Mastodon API: Marking a conversation as read (`POST /api/v1/conversations/:id/read`) now no longer brings it to the top in the user's direct conversation list
</details> </details>
## [1.1.9] - 2020-02-10
### Fixed
- OTP: Inability to set the upload limit (again)
- Not being able to pin polls
- Streaming API: incorrect handling of reblog mutes
- Rejecting the user when field length limit is exceeded
- OpenGraph provider: html entities in descriptions
## [1.1.8] - 2020-01-10
### Fixed
- Captcha generation issues
- Returned Kocaptcha endpoint to configuration
- Captcha validity is now 5 minutes
## [1.1.7] - 2019-12-13
### Fixed
- OTP: Inability to set the upload limit
- OTP: Inability to override node name/distribution type to run 2 Pleroma instances on the same machine
### Added
- Integrated captcha provider
### Changed
- Captcha enabled by default
- Default Captcha provider changed from `Pleroma.Captcha.Kocaptcha` to `Pleroma.Captcha.Native`
- Better `Cache-Control` header for static content
### Bundled Pleroma-FE Changes
#### Added
- Icons in the navigation panel
#### Fixed
- Improved support unauthenticated view of private instances
#### Removed
- Whitespace hack on empty post content
## [1.1.6] - 2019-11-19 ## [1.1.6] - 2019-11-19
### Fixed ### Fixed
- Not being able to log into to third party apps when the browser is logged into mastofe - Not being able to log into to third party apps when the browser is logged into mastofe

View file

@ -22,9 +22,10 @@ def generate_like_activities(user, posts) do
def generate_users(opts) do def generate_users(opts) do
IO.puts("Starting generating #{opts[:users_max]} users...") IO.puts("Starting generating #{opts[:users_max]} users...")
{time, _} = :timer.tc(fn -> do_generate_users(opts) end) {time, users} = :timer.tc(fn -> do_generate_users(opts) end)
IO.puts("Inserting users take #{to_sec(time)} sec.\n") IO.puts("Inserting users took #{to_sec(time)} sec.\n")
users
end end
defp do_generate_users(opts) do defp do_generate_users(opts) do

View file

@ -0,0 +1,76 @@
defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do
use Mix.Task
alias Pleroma.Repo
alias Pleroma.LoadTesting.Generator
alias Pleroma.Web.CommonAPI
def run(_args) do
Mix.Pleroma.start_pleroma()
# Cleaning tables
clean_tables()
[{:ok, user} | users] = Generator.generate_users(users_max: 1000)
# Let the user make 100 posts
1..100
|> Enum.each(fn i -> CommonAPI.post(user, %{"status" => to_string(i)}) end)
# Let 10 random users post
posts =
users
|> Enum.take_random(10)
|> Enum.map(fn {:ok, random_user} ->
{:ok, activity} = CommonAPI.post(random_user, %{"status" => "."})
activity
end)
# let our user repeat them
posts
|> Enum.each(fn activity ->
CommonAPI.repeat(activity.id, user)
end)
Benchee.run(
%{
"user timeline, no followers" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Plug.Conn.assign(:user, reading_user)
|> Plug.Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
users
|> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end)
Benchee.run(
%{
"user timeline, all following" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Plug.Conn.assign(:user, reading_user)
|> Plug.Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
end
defp clean_tables do
IO.puts("Deleting old data...\n")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
end
end

View file

@ -61,8 +61,6 @@
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma_job_queue, disabled: true
config :pleroma, Pleroma.ScheduledActivity, config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2, daily_user_limit: 2,
total_user_limit: 3, total_user_limit: 3,

View file

@ -504,10 +504,6 @@
federator_outgoing: 5 federator_outgoing: 5
] ]
config :pleroma, :fetch_initial_posts,
enabled: false,
pages: 5
config :auto_linker, config :auto_linker,
opts: [ opts: [
extra: true, extra: true,
@ -628,6 +624,11 @@
parameters: [gin_fuzzy_search_limit: "500"], parameters: [gin_fuzzy_search_limit: "500"],
prepare: :unnamed prepare: :unnamed
config :pleroma, :restrict_unauthenticated,
timelines: %{local: false, federated: false},
profiles: %{local: false, remote: false},
activities: %{local: false, remote: false}
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -1780,25 +1780,6 @@
} }
] ]
}, },
%{
group: :pleroma_job_queue,
key: :queues,
type: :group,
description: "[Deprecated] Replaced with `Oban`/`:queues` (keeping the same format)"
},
%{
group: :pleroma,
key: Pleroma.Web.Federator.RetryQueue,
type: :group,
description: "[Deprecated] See `Oban` and `:workers` sections for configuration notes",
children: [
%{
key: :max_retries,
type: :integer,
description: "[Deprecated] Replaced as `Oban`/`:queues`/`:outgoing_federation` value"
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: Oban, key: Oban,
@ -2007,25 +1988,6 @@
} }
] ]
}, },
%{
group: :pleroma,
key: :fetch_initial_posts,
type: :group,
description: "Fetching initial posts settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Fetch posts when a new user is federated with"
},
%{
key: :pages,
type: :integer,
description: "The amount of pages to fetch",
suggestions: [5]
}
]
},
%{ %{
group: :auto_linker, group: :auto_linker,
key: :opts, key: :opts,
@ -2480,7 +2442,7 @@
%{ %{
key: :relations_actions, key: :relations_actions,
type: [:tuple, {:list, :tuple}], type: [:tuple, {:list, :tuple}],
description: "For actions on relations with all users (follow, unfollow)", description: "For actions on relationships with all users (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]] suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
}, },
%{ %{
@ -2596,19 +2558,6 @@
} }
] ]
}, },
%{
group: :tesla,
type: :group,
description: "Tesla settings",
children: [
%{
key: :adapter,
type: :module,
description: "Tesla adapter",
suggestions: [Tesla.Adapter.Hackney]
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: :chat, key: :chat,
@ -2966,5 +2915,65 @@
suggestions: [2] suggestions: [2]
} }
] ]
},
%{
group: :pleroma,
key: :restrict_unauthenticated,
type: :group,
description:
"Disallow viewing timelines, user profiles and statuses for unauthenticated users.",
children: [
%{
key: :timelines,
type: :map,
description: "Settings for public and federated timelines.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view public timeline."
},
%{
key: :federated,
type: :boolean,
description: "Disallow view federated timeline."
}
]
},
%{
key: :profiles,
type: :map,
description: "Settings for user profiles.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local user profiles."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote user profiles."
}
]
},
%{
key: :activities,
type: :map,
description: "Settings for statuses.",
children: [
%{
key: :local,
type: :boolean,
description: "Disallow view local statuses."
},
%{
key: :remote,
type: :boolean,
description: "Disallow view remote statuses."
}
]
}
]
} }
] ]

View file

@ -92,6 +92,8 @@
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -414,6 +414,83 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nicknames` - `nicknames`
- Response: none (code `204`) - Response: none (code `204`)
## `GET /api/pleroma/admin/users/:nickname/credentials`
### Get the user's email, password, display and settings-related fields
- Params:
- `nickname`
- Response:
```json
{
"actor_type": "Person",
"allow_following_move": true,
"avatar": "https://pleroma.social/media/7e8e7508fd545ef580549b6881d80ec0ff2c81ed9ad37b9bdbbdf0e0d030159d.jpg",
"background": "https://pleroma.social/media/4de34c0bd10970d02cbdef8972bef0ebbf55f43cadc449554d4396156162fe9a.jpg",
"banner": "https://pleroma.social/media/8d92ba2bd244b613520abf557dd448adcd30f5587022813ee9dd068945986946.jpg",
"bio": "bio",
"default_scope": "public",
"discoverable": false,
"email": "user@example.com",
"fields": [
{
"name": "example",
"value": "<a href=\"https://example.com\" rel=\"ugc\">https://example.com</a>"
}
],
"hide_favorites": false,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"id": "9oouHaEEUR54hls968",
"locked": true,
"name": "user",
"no_rich_text": true,
"pleroma_settings_store": {},
"raw_fields": [
{
"id": 1,
"name": "example",
"value": "https://example.com"
},
],
"show_role": true,
"skip_thread_containment": false
}
```
## `PATCH /api/pleroma/admin/users/:nickname/credentials`
### Change the user's email, password, display and settings-related fields
- Params:
- `email`
- `password`
- `name`
- `bio`
- `avatar`
- `locked`
- `no_rich_text`
- `default_scope`
- `banner`
- `hide_follows`
- `hide_followers`
- `hide_followers_count`
- `hide_follows_count`
- `hide_favorites`
- `allow_following_move`
- `background`
- `show_role`
- `skip_thread_containment`
- `fields`
- `discoverable`
- `actor_type`
- Response: none (code `200`)
## `GET /api/pleroma/admin/reports` ## `GET /api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports

View file

@ -117,7 +117,7 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
Accepts additional parameters: Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
- `with_move`: boolean, when set to `true` will include Move notifications. `false` by default. - `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
## POST `/api/v1/statuses` ## POST `/api/v1/statuses`
@ -180,7 +180,7 @@ Post here request with grant_type=refresh_token to obtain new access token. Retu
## Account Registration ## Account Registration
`POST /api/v1/accounts` `POST /api/v1/accounts`
Has theses additionnal parameters (which are the same as in Pleroma-API): Has theses additional parameters (which are the same as in Pleroma-API):
* `fullname`: optional * `fullname`: optional
* `bio`: optional * `bio`: optional
* `captcha_solution`: optional, contains provider-specific captcha solution, * `captcha_solution`: optional, contains provider-specific captcha solution,

View file

@ -288,10 +288,11 @@ Pleroma Conversations have the same general structure that Mastodon Conversation
2. Pleroma Conversations statuses can be requested by Conversation id. 2. Pleroma Conversations statuses can be requested by Conversation id.
3. Pleroma Conversations can be replied to. 3. Pleroma Conversations can be replied to.
Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation. Conversations have the additional field `recipients` under the `pleroma` key. This holds a list of all the accounts that will receive a message in this conversation.
The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation. The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
⚠ Conversation IDs can be found in direct messages with the `pleroma.direct_conversation_id` key, do not confuse it with `pleroma.conversation_id`.
## `GET /api/v1/pleroma/conversations/:id/statuses` ## `GET /api/v1/pleroma/conversations/:id/statuses`
### Timeline for a given conversation ### Timeline for a given conversation

View file

@ -10,11 +10,11 @@
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl database remove_embedded_objects [<options>] ./bin/pleroma_ctl database remove_embedded_objects [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.database remove_embedded_objects [<options>] mix pleroma.database remove_embedded_objects [option ...]
``` ```
### Options ### Options
@ -28,11 +28,11 @@ This will prune remote posts older than 90 days (configurable with [`config :ple
The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free. The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free.
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl database prune_objects [<options>] ./bin/pleroma_ctl database prune_objects [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.database prune_objects [<options>] mix pleroma.database prune_objects [option ...]
``` ```
### Options ### Options

View file

@ -5,11 +5,11 @@
## Send digest email since given date (user registration date by default) ignoring user activity status. ## Send digest email since given date (user registration date by default) ignoring user activity status.
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl digest test <nickname> [<since_date>] ./bin/pleroma_ctl digest test <nickname> [since_date]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.digest test <nickname> [<since_date>] mix pleroma.digest test <nickname> [since_date]
``` ```

View file

@ -5,11 +5,11 @@
## Lists emoji packs and metadata specified in the manifest ## Lists emoji packs and metadata specified in the manifest
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl emoji ls-packs [<options>] ./bin/pleroma_ctl emoji ls-packs [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.emoji ls-packs [<options>] mix pleroma.emoji ls-packs [option ...]
``` ```
@ -19,11 +19,11 @@ mix pleroma.emoji ls-packs [<options>]
## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME` ## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME`
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl emoji get-packs [<options>] <packs> ./bin/pleroma_ctl emoji get-packs [option ...] <pack ...>
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.emoji get-packs [<options>] <packs> mix pleroma.emoji get-packs [option ...] <pack ...>
``` ```
### Options ### Options

View file

@ -4,11 +4,11 @@
## Generate a new configuration file ## Generate a new configuration file
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl instance gen [<options>] ./bin/pleroma_ctl instance gen [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.instance gen [<options>] mix pleroma.instance gen [option ...]
``` ```

View file

@ -4,11 +4,11 @@
## Migrate uploads from local to remote storage ## Migrate uploads from local to remote storage
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl uploads migrate_local <target_uploader> [<options>] ./bin/pleroma_ctl uploads migrate_local <target_uploader> [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.uploads migrate_local <target_uploader> [<options>] mix pleroma.uploads migrate_local <target_uploader> [option ...]
``` ```
### Options ### Options

View file

@ -5,11 +5,11 @@
## Create a user ## Create a user
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user new <email> [<options>] ./bin/pleroma_ctl user new <nickname> <email> [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.user new <email> [<options>] mix pleroma.user new <nickname> <email> [option ...]
``` ```
@ -33,11 +33,11 @@ mix pleroma.user list
## Generate an invite link ## Generate an invite link
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user invite [<options>] ./bin/pleroma_ctl user invite [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.user invite [<options>] mix pleroma.user invite [option ...]
``` ```
@ -137,11 +137,11 @@ mix pleroma.user reset_password <nickname>
## Set the value of the given user's settings ## Set the value of the given user's settings
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user set <nickname> [<options>] ./bin/pleroma_ctl user set <nickname> [option ...]
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.user set <nickname> [<options>] mix pleroma.user set <nickname> [option ...]
``` ```
### Options ### Options

View file

@ -18,9 +18,8 @@
6. Run `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>` 6. Run `sudo -Hu postgres pg_restore -d <pleroma_db> -v -1 </path/to/backup_location/pleroma.pgdump>`
7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any. 7. If you installed a newer Pleroma version, you should run `mix ecto.migrate`[^1]. This task performs database migrations, if there were any.
8. Restart the Pleroma service. 8. Restart the Pleroma service.
9. After you've restarted Pleroma, you will notice that postgres will take up more cpu resources than usual. A lot in fact. To fix this you must do a VACUUM ANLAYZE. This can also be done while the instance is still running like so: 9. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries.
$ sudo -u postgres psql pleroma_database_name
pleroma=# VACUUM ANALYZE;
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file. [^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
## Remove ## Remove

View file

@ -1,5 +1,5 @@
# Pleroma Clients # Pleroma Clients
Note: Additionnal clients may be working but theses are officially supporting Pleroma. Note: Additional clients may be working but theses are officially supporting Pleroma.
Feel free to contact us to be added to this list! Feel free to contact us to be added to this list!
## Desktop ## Desktop

View file

@ -138,7 +138,8 @@ config :pleroma, :mrf_user_allowlist,
``` ```
#### :mrf_object_age #### :mrf_object_age
* `threshold`: Required age (in seconds) of a post before actions are taken. * `threshold`: Required time offset (in seconds) compared to your server clock of an incoming post before actions are taken.
e.g., A value of 900 results in any post with a timestamp older than 15 minutes will be acted upon.
* `actions`: A list of actions to apply to the post: * `actions`: A list of actions to apply to the post:
* `:delist` removes the post from public timelines * `:delist` removes the post from public timelines
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
@ -151,14 +152,6 @@ config :pleroma, :mrf_user_allowlist,
* `sign_object_fetches`: Sign object fetches with HTTP signatures * `sign_object_fetches`: Sign object fetches with HTTP signatures
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches * `authorized_fetch_mode`: Require HTTP signatures for AP fetches
### :fetch_initial_posts
!!! warning
Be careful with this setting, fetching posts may lead to new users being discovered whose posts will then also be fetched. This can lead to serious load on your instance and database.
* `enabled`: If enabled, when a new user is discovered by your instance, fetch some of their latest posts.
* `pages`: The amount of pages to fetch
## Pleroma.ScheduledActivity ## Pleroma.ScheduledActivity
* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`) * `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`)
@ -879,3 +872,21 @@ config :auto_linker,
## :configurable_from_database ## :configurable_from_database
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information. Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
## Restrict entities access for unauthenticated users
### :restrict_unauthenticated
Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
* `timelines` - public and federated timelines
* `local` - public timeline
* `federated`
* `profiles` - user profiles
* `local`
* `remote`
* `activities` - statuses
* `local`
* `remote`

View file

@ -156,8 +156,8 @@ cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf
``` ```
```sh tab="Debian/Ubuntu" ```sh tab="Debian/Ubuntu"
cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.conf
ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx ln -s /etc/nginx/sites-available/pleroma.conf /etc/nginx/sites-enabled/pleroma.conf
``` ```
If your distro does not have either of those you can append `include /etc/nginx/pleroma.conf` to the end of the http section in /etc/nginx/nginx.conf and If your distro does not have either of those you can append `include /etc/nginx/pleroma.conf` to the end of the http section in /etc/nginx/nginx.conf and

View file

@ -90,8 +90,6 @@ server {
proxy_ignore_client_abort on; proxy_ignore_client_abort on;
proxy_buffering on; proxy_buffering on;
chunked_transfer_encoding on; chunked_transfer_encoding on;
proxy_ignore_headers Cache-Control;
proxy_hide_header Cache-Control;
proxy_pass http://127.0.0.1:4000; proxy_pass http://127.0.0.1:4000;
} }
} }

View file

@ -28,7 +28,7 @@ def run(_) do
defp do_run(implementation) do defp do_run(implementation) do
start_pleroma() start_pleroma()
with descriptions <- Pleroma.Config.Loader.load("config/description.exs"), with descriptions <- Pleroma.Config.Loader.read("config/description.exs"),
{:ok, file_path} <- {:ok, file_path} <-
Pleroma.Docs.Generator.process( Pleroma.Docs.Generator.process(
implementation, implementation,

View file

@ -35,7 +35,7 @@ def run(["unfollow", target]) do
def run(["list"]) do def run(["list"]) do
start_pleroma() start_pleroma()
with {:ok, list} <- Relay.list() do with {:ok, list} <- Relay.list(true) do
list |> Enum.each(&shell_info(&1)) list |> Enum.each(&shell_info(&1))
else else
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")

View file

@ -95,6 +95,17 @@ def with_preloaded_object(query, join_type \\ :inner) do
|> preload([activity, object: object], object: object) |> preload([activity, object: object], object: object)
end end
# Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
def user_actor(%Activity{actor: nil}), do: nil
def user_actor(%Activity{} = activity) do
with %User{} <- activity.user_actor do
activity.user_actor
else
_ -> User.get_cached_by_ap_id(activity.actor)
end
end
def with_joined_user_actor(query, join_type \\ :inner) do def with_joined_user_actor(query, join_type \\ :inner) do
join(query, join_type, [activity], u in User, join(query, join_type, [activity], u in User,
on: u.ap_id == activity.actor, on: u.ap_id == activity.actor,
@ -308,6 +319,13 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
|> where([a], fragment("? ->> 'state' = 'pending'", a.data)) |> where([a], fragment("? ->> 'state' = 'pending'", a.data))
end end
def following_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
Queries.by_type("Follow")
|> where([a], fragment("?->>'state' = 'pending'", a.data))
|> where([a], a.actor == ^ap_id)
|> Repo.all()
end
def restrict_deactivated_users(query) do def restrict_deactivated_users(query) do
deactivated_users = deactivated_users =
from(u in User.Query.build(%{deactivated: true}), select: u.ap_id) from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)

View file

@ -39,7 +39,7 @@ defp visibility_tags(object, activity) do
end end
end end
defp item_creation_tags(tags, %{data: %{"type" => "Create"}} = object, activity) do defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) tags ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
end end

View file

@ -35,6 +35,13 @@ def by_author(query \\ Activity, %User{ap_id: ap_id}) do
from(a in query, where: a.actor == ^ap_id) from(a in query, where: a.actor == ^ap_id)
end end
def find_by_object_ap_id(activities, object_ap_id) do
Enum.find(
activities,
&(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]])
)
end
@spec by_object_id(query, String.t() | [String.t()]) :: query @spec by_object_id(query, String.t() | [String.t()]) :: query
def by_object_id(query \\ Activity, object_id) def by_object_id(query \\ Activity, object_id)

View file

@ -31,6 +31,7 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
Pleroma.Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers() Pleroma.HTML.compile_scrubbers()
Pleroma.Config.DeprecationWarnings.warn() Pleroma.Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()

View file

@ -3,14 +3,33 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Holder do defmodule Pleroma.Config.Holder do
@config Pleroma.Config.Loader.load_and_merge() @config Pleroma.Config.Loader.default_config()
@spec config() :: keyword() @spec save_default() :: :ok
def config, do: @config def save_default do
default_config =
if System.get_env("RELEASE_NAME") do
release_config =
[:code.root_dir(), "releases", System.get_env("RELEASE_VSN"), "releases.exs"]
|> Path.join()
|> Pleroma.Config.Loader.read()
@spec config(atom()) :: any() Pleroma.Config.Loader.merge(@config, release_config)
def config(group), do: @config[group] else
@config
end
@spec config(atom(), atom()) :: any() Pleroma.Config.put(:default_config, default_config)
def config(group, key), do: @config[group][key] end
@spec default_config() :: keyword()
def default_config, do: get_default()
@spec default_config(atom()) :: keyword()
def default_config(group), do: Keyword.get(get_default(), group)
@spec default_config(atom(), atom()) :: keyword()
def default_config(group, key), do: get_in(get_default(), [group, key])
defp get_default, do: Pleroma.Config.get(:default_config)
end end

View file

@ -13,32 +13,28 @@ defmodule Pleroma.Config.Loader do
] ]
if Code.ensure_loaded?(Config.Reader) do if Code.ensure_loaded?(Config.Reader) do
@spec load(Path.t()) :: keyword() @reader Config.Reader
def load(path), do: Config.Reader.read!(path)
defp do_merge(conf1, conf2), do: Config.Reader.merge(conf1, conf2) def read(path), do: @reader.read!(path)
else else
# support for Elixir less than 1.9 # support for Elixir less than 1.9
@spec load(Path.t()) :: keyword() @reader Mix.Config
def load(path) do def read(path) do
path path
|> Mix.Config.eval!() |> @reader.eval!()
|> elem(0) |> elem(0)
end end
defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2)
end end
@spec load_and_merge() :: keyword() @spec read(Path.t()) :: keyword()
def load_and_merge do
all_paths =
if Pleroma.Config.get(:release),
do: ["config/config.exs", "config/releases.exs"],
else: ["config/config.exs"]
all_paths @spec merge(keyword(), keyword()) :: keyword()
|> Enum.map(&load(&1)) def merge(c1, c2), do: @reader.merge(c1, c2)
|> Enum.reduce([], &do_merge(&2, &1))
@spec default_config() :: keyword()
def default_config do
"config/config.exs"
|> read()
|> filter() |> filter()
end end

View file

@ -83,7 +83,7 @@ defp merge_and_update(setting) do
key = ConfigDB.from_string(setting.key) key = ConfigDB.from_string(setting.key)
group = ConfigDB.from_string(setting.group) group = ConfigDB.from_string(setting.group)
default = Pleroma.Config.Holder.config(group, key) default = Pleroma.Config.Holder.default_config(group, key)
value = ConfigDB.from_binary(setting.value) value = ConfigDB.from_binary(setting.value)
merged_value = merged_value =

View file

@ -129,21 +129,18 @@ def for_user(user, params \\ %{}) do
end end
def restrict_recipients(query, user, %{"recipients" => user_ids}) do def restrict_recipients(query, user, %{"recipients" => user_ids}) do
user_ids = user_binary_ids =
[user.id | user_ids] [user.id | user_ids]
|> Enum.uniq() |> Enum.uniq()
|> Enum.reduce([], fn user_id, acc -> |> User.binary_id()
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
[user_id | acc]
end)
conversation_subquery = conversation_subquery =
__MODULE__ __MODULE__
|> group_by([p], p.conversation_id) |> group_by([p], p.conversation_id)
|> having( |> having(
[p], [p],
count(p.user_id) == ^length(user_ids) and count(p.user_id) == ^length(user_binary_ids) and
fragment("array_agg(?) @> ?", p.user_id, ^user_ids) fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids)
) )
|> select([p], %{id: p.conversation_id}) |> select([p], %{id: p.conversation_id})

View file

@ -15,7 +15,7 @@ def process(descriptions) do
end end
def compile do def compile do
with config <- Pleroma.Config.Loader.load("config/description.exs") do with config <- Pleroma.Config.Loader.read("config/description.exs") do
config[:pleroma][:config_description] config[:pleroma][:config_description]
|> Pleroma.Docs.Generator.convert_to_strings() |> Pleroma.Docs.Generator.convert_to_strings()
|> Jason.encode!() |> Jason.encode!()

View file

@ -0,0 +1,256 @@
# Pleroma: A lightweight social networking server
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
#
# This file is derived from Earmark, under the following copyright:
# Copyright © 2014 Dave Thomas, The Pragmatic Programmers
# SPDX-License-Identifier: Apache-2.0
# Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex
defmodule Pleroma.EarmarkRenderer do
@moduledoc false
alias Earmark.Block
alias Earmark.Context
alias Earmark.HtmlRenderer
alias Earmark.Options
import Earmark.Inline, only: [convert: 3]
import Earmark.Helpers.HtmlHelpers
import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2]
import Earmark.Context, only: [append: 2, set_value: 2]
import Earmark.Options, only: [get_mapper: 1]
@doc false
def render(blocks, %Context{options: %Options{}} = context) do
messages = get_messages(context)
{contexts, html} =
get_mapper(context.options).(
blocks,
&render_block(&1, put_in(context.options.messages, []))
)
|> Enum.unzip()
all_messages =
contexts
|> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end)
{put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()}
end
#############
# Paragraph #
#############
defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do
lines = convert(lines, lnb, context)
add_attrs(lines, "<p>#{lines.value}</p>", attrs, [], lnb)
end
########
# Html #
########
defp render_block(%Block.Html{html: html}, context) do
{context, html}
end
defp render_block(%Block.HtmlComment{lines: lines}, context) do
{context, lines}
end
defp render_block(%Block.HtmlOneline{html: html}, context) do
{context, html}
end
#########
# Ruler #
#########
defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do
add_attrs(context, "<hr />", attrs, [], lnb)
end
###########
# Heading #
###########
defp render_block(
%Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs},
context
) do
converted = convert(content, lnb, context)
html = "<h#{level}>#{converted.value}</h#{level}>"
add_attrs(converted, html, attrs, [], lnb)
end
##############
# Blockquote #
##############
defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
{context1, body} = render(blocks, context)
html = "<blockquote>#{body}</blockquote>"
add_attrs(context1, html, attrs, [], lnb)
end
#########
# Table #
#########
defp render_block(
%Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs},
context
) do
{context1, html} = add_attrs(context, "<table>", attrs, [], lnb)
context2 = set_value(context1, html)
context3 =
if header do
append(add_trs(append(context2, "<thead>"), [header], "th", aligns, lnb), "</thead>")
else
# Maybe an error, needed append(context, html)
context2
end
context4 = append(add_trs(append(context3, "<tbody>"), rows, "td", aligns, lnb), "</tbody>")
{context4, [context4.value, "</table>"]}
end
########
# Code #
########
defp render_block(
%Block.Code{lnb: lnb, language: language, attrs: attrs} = block,
%Context{options: options} = context
) do
class =
if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: ""
tag = ~s[<pre><code#{class}>]
lines = options.render_code.(block)
html = ~s[#{tag}#{lines}</code></pre>]
add_attrs(context, html, attrs, [], lnb)
end
#########
# Lists #
#########
defp render_block(
%Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start},
context
) do
{context1, content} = render(items, context)
html = "<#{type}#{start}>#{content}</#{type}>"
add_attrs(context1, html, attrs, [], lnb)
end
# format a single paragraph list item, and remove the para tags
defp render_block(
%Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs},
context
)
when length(blocks) == 1 do
{context1, content} = render(blocks, context)
content = Regex.replace(~r{</?p>}, content, "")
html = "<li>#{content}</li>"
add_attrs(context1, html, attrs, [], lnb)
end
# format a spaced list item
defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
{context1, content} = render(blocks, context)
html = "<li>#{content}</li>"
add_attrs(context1, html, attrs, [], lnb)
end
##################
# Footnote Block #
##################
defp render_block(%Block.FnList{blocks: footnotes}, context) do
items =
Enum.map(footnotes, fn note ->
blocks = append_footnote_link(note)
%Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks}
end)
{context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context)
{context1, Enum.join([~s[<div class="footnotes">], "<hr />", html, "</div>"])}
end
#######################################
# Isolated IALs are rendered as paras #
#######################################
defp render_block(%Block.Ial{verbatim: verbatim}, context) do
{context, "<p>{:#{verbatim}}</p>"}
end
####################
# IDDef is ignored #
####################
defp render_block(%Block.IdDef{}, context), do: {context, ""}
#####################################
# And here are the inline renderers #
#####################################
defdelegate br, to: HtmlRenderer
defdelegate codespan(text), to: HtmlRenderer
defdelegate em(text), to: HtmlRenderer
defdelegate strong(text), to: HtmlRenderer
defdelegate strikethrough(text), to: HtmlRenderer
defdelegate link(url, text), to: HtmlRenderer
defdelegate link(url, text, title), to: HtmlRenderer
defdelegate image(path, alt, title), to: HtmlRenderer
defdelegate footnote_link(ref, backref, number), to: HtmlRenderer
# Table rows
defp add_trs(context, rows, tag, aligns, lnb) do
numbered_rows =
rows
|> Enum.zip(Stream.iterate(lnb, &(&1 + 1)))
numbered_rows
|> Enum.reduce(context, fn {row, lnb}, ctx ->
append(add_tds(append(ctx, "<tr>"), row, tag, aligns, lnb), "</tr>")
end)
end
defp add_tds(context, row, tag, aligns, lnb) do
Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb))
end
defp add_td_fn(row, tag, aligns, lnb) do
fn n, ctx ->
style =
case Enum.at(aligns, n - 1, :default) do
:default -> ""
align -> " style=\"text-align: #{align}\""
end
col = Enum.at(row, n - 1)
converted = convert(col, lnb, set_messages(ctx, []))
append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}</#{tag}>")
end
end
###############################
# Append Footnote Return Link #
###############################
defdelegate append_footnote_link(note), to: HtmlRenderer
defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer
defdelegate render_code(lines), to: HtmlRenderer
defp code_classes(language, prefix) do
["" | String.split(prefix || "")]
|> Enum.map(fn pfx -> "#{pfx}#{language}" end)
|> Enum.join(" ")
end
end

View file

@ -129,4 +129,32 @@ def move_following(origin, target) do
move_following(origin, target) move_following(origin, target)
end end
end end
def all_between_user_sets(
source_users,
target_users
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
__MODULE__
|> where(
fragment(
"(follower_id = ANY(?) AND following_id = ANY(?)) OR \
(follower_id = ANY(?) AND following_id = ANY(?))",
^source_user_ids,
^target_user_ids,
^target_user_ids,
^source_user_ids
)
)
|> Repo.all()
end
def find(following_relationships, follower, following) do
Enum.find(following_relationships, fn
fr -> fr.follower_id == follower.id and fr.following_id == following.id
end)
end
end end

View file

@ -387,24 +387,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}" "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
"subject" => user
}
})
when is_map(user) do
get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
"subject" => [user]
}
})
end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
@ -416,24 +398,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}" "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
"subject" => user
}
})
when is_map(user) do
get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
"subject" => [user]
}
})
end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
@ -473,26 +437,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}" "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
"subject" => user,
"permission" => permission
}
})
when is_map(user) do
get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
"subject" => [user],
"permission" => permission
}
})
end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
@ -505,26 +449,6 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}" "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
"subject" => user,
"permission" => permission
}
})
when is_map(user) do
get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
"subject" => [user],
"permission" => permission
}
})
end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
@ -681,6 +605,17 @@ def get_log_entry_message(%ModerationLog{
}" }"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "updated_users",
"subject" => subjects
}
}) do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
defp nicknames_to_string(nicknames) do defp nicknames_to_string(nicknames) do
nicknames nicknames
|> Enum.map(&"@#{&1}") |> Enum.map(&"@#{&1}")

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Notification do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push alias Pleroma.Web.Push
@ -19,6 +20,7 @@ defmodule Pleroma.Notification do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
require Logger require Logger
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -50,11 +52,11 @@ def last_read_query(user) do
end end
defp for_user_query_ap_id_opts(user, opts) do defp for_user_query_ap_id_opts(user, opts) do
ap_id_relations = ap_id_relationships =
[:block] ++ [:block] ++
if opts[@include_muted_option], do: [], else: [:notification_mute] if opts[@include_muted_option], do: [], else: [:notification_mute]
preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
@ -90,7 +92,6 @@ def for_user_query(user, opts \\ %{}) do
|> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts) |> exclude_blocked(user, exclude_blocked_opts)
|> exclude_visibility(opts) |> exclude_visibility(opts)
|> exclude_move(opts)
end end
defp exclude_blocked(query, user, opts) do defp exclude_blocked(query, user, opts) do
@ -114,20 +115,12 @@ defp exclude_notification_muted(query, user, opts) do
query query
|> where([n, a], a.actor not in ^notification_muted_ap_ids) |> where([n, a], a.actor not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in Pleroma.ThreadMute, |> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
) )
|> where([n, a, o, tm], is_nil(tm.user_id)) |> where([n, a, o, tm], is_nil(tm.user_id))
end end
defp exclude_move(query, %{with_move: true}) do
query
end
defp exclude_move(query, _opts) do
where(query, [n, a], fragment("?->>'type' != 'Move'", a.data))
end
@valid_visibilities ~w[direct unlisted public private] @valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility}) defp exclude_visibility(query, %{exclude_visibilities: visibility})
@ -302,32 +295,35 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity) object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do if object && object.data["type"] == "Answer" do
notifications =
activity
|> get_notified_from_activity()
|> Enum.map(&create_notification(activity, &1))
{:ok, notifications}
else
{:ok, []} {:ok, []}
else
do_create_notifications(activity)
end end
end end
def create_notifications(%Activity{data: %{"type" => type}} = activity) def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
notifications = do_create_notifications(activity)
activity
|> get_notified_from_activity()
|> Enum.map(&create_notification(activity, &1))
{:ok, notifications}
end end
def create_notifications(_), do: {:ok, []} def create_notifications(_), do: {:ok, []}
defp do_create_notifications(%Activity{} = activity) do
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications =
Enum.map(potential_receivers, fn user ->
do_send = user in enabled_receivers
create_notification(activity, user, do_send)
end)
{:ok, notifications}
end
# TODO move to sql, too. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do unless skip?(activity, user) do
{:ok, %{notification: notification}} = {:ok, %{notification: notification}} =
Multi.new() Multi.new()
@ -335,31 +331,78 @@ def create_notification(%Activity{} = activity, %User{} = user) do
|> Marker.multi_set_last_read_id(user, "notifications") |> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction() |> Repo.transaction()
["user", "user:notification"] if do_send do
|> Streamer.stream(notification) Streamer.stream(["user", "user:notification"], notification)
Push.send(notification)
end
Push.send(notification)
notification notification
end end
end end
@doc """
Returns a tuple with 2 elements:
{enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
[] potential_receiver_ap_ids =
|> Utils.maybe_notify_to_recipients(activity) []
|> Utils.maybe_notify_mentioned_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_subscribers(activity) |> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_followers(activity) |> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq() |> Utils.maybe_notify_followers(activity)
|> User.get_users_from_set(local_only) |> Enum.uniq()
# Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
notification_enabled_ap_ids =
potential_receiver_ap_ids
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
potential_receivers =
potential_receiver_ap_ids
|> Enum.uniq()
|> User.get_users_from_set(local_only)
notification_enabled_users =
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
{notification_enabled_users, potential_receivers -- notification_enabled_users}
end end
def get_notified_from_activity(_, _local_only), do: [] def get_notified_from_activity(_, _local_only), do: {[], []}
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
def exclude_relationship_restricted_ap_ids([], _activity), do: []
def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
relationship_restricted_ap_ids =
activity
|> Activity.user_actor()
|> User.incoming_relationships_ungrouped_ap_ids([
:block,
:notification_mute
])
Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
end
@doc "Filters out AP IDs of users who mute activity thread"
def exclude_thread_muter_ap_ids([], _activity), do: []
def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
Enum.uniq(ap_ids) -- thread_muter_ap_ids
end
@spec skip?(Activity.t(), User.t()) :: boolean() @spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do def skip?(%Activity{} = activity, %User{} = user) do
[ [
:self, :self,
:followers, :followers,
@ -368,18 +411,20 @@ def skip?(activity, user) do
:non_follows, :non_follows,
:recently_followed :recently_followed
] ]
|> Enum.any?(&skip?(&1, activity, user)) |> Enum.find(&skip?(&1, activity, user))
end end
def skip?(_, _), do: false
@spec skip?(atom(), Activity.t(), User.t()) :: boolean() @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
def skip?(:self, activity, user) do def skip?(:self, %Activity{} = activity, %User{} = user) do
activity.data["actor"] == user.ap_id activity.data["actor"] == user.ap_id
end end
def skip?( def skip?(
:followers, :followers,
activity, %Activity{} = activity,
%{notification_settings: %{followers: false}} = user %User{notification_settings: %{followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
@ -388,15 +433,19 @@ def skip?(
def skip?( def skip?(
:non_followers, :non_followers,
activity, %Activity{} = activity,
%{notification_settings: %{non_followers: false}} = user %User{notification_settings: %{non_followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user) !User.following?(follower, user)
end end
def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do def skip?(
:follows,
%Activity{} = activity,
%User{notification_settings: %{follows: false}} = user
) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed) User.following?(user, followed)
@ -404,15 +453,16 @@ def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user
def skip?( def skip?(
:non_follows, :non_follows,
activity, %Activity{} = activity,
%{notification_settings: %{non_follows: false}} = user %User{notification_settings: %{non_follows: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed) !User.following?(user, followed)
end end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]
Notification.for_user(user) Notification.for_user(user)

View file

@ -15,9 +15,24 @@ def call(%{assigns: %{user: %User{}}} = conn, _) do
conn conn
end end
def call(conn, _) do def call(conn, options) do
perform =
cond do
options[:if_func] -> options[:if_func].()
options[:unless_func] -> !options[:unless_func].()
true -> true
end
if perform do
fail(conn)
else
conn
end
end
def fail(conn) do
conn conn
|> render_error(:forbidden, "Invalid credentials.") |> render_error(:forbidden, "Invalid credentials.")
|> halt |> halt()
end end
end end

View file

@ -10,14 +10,20 @@ def init(options) do
end end
def call(conn, _opts) do def call(conn, _opts) do
if Pleroma.Config.get([:instance, :federating]) do if federating?() do
conn conn
else else
conn fail(conn)
|> put_status(404)
|> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
|> Phoenix.Controller.render("404.json")
|> halt()
end end
end end
def federating?, do: Pleroma.Config.get([:instance, :federating])
defp fail(conn) do
conn
|> put_status(404)
|> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
|> Phoenix.Controller.render("404.json")
|> halt()
end
end end

View file

@ -78,7 +78,7 @@ def init(plug_opts) do
end end
def call(conn, plug_opts) do def call(conn, plug_opts) do
if disabled?() do if disabled?(conn) do
handle_disabled(conn) handle_disabled(conn)
else else
action_settings = action_settings(plug_opts) action_settings = action_settings(plug_opts)
@ -87,9 +87,9 @@ def call(conn, plug_opts) do
end end
defp handle_disabled(conn) do defp handle_disabled(conn) do
if Config.get(:env) == :prod do Logger.warn(
Logger.warn("Rate limiter is disabled for localhost/socket") "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
end )
conn conn
end end
@ -109,16 +109,21 @@ defp handle(conn, action_settings) do
end end
end end
def disabled? do def disabled?(conn) do
localhost_or_socket = localhost_or_socket =
Config.get([Pleroma.Web.Endpoint, :http, :ip]) case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
|> Tuple.to_list() {127, 0, 0, 1} -> true
|> Enum.join(".") {0, 0, 0, 0, 0, 0, 0, 1} -> true
|> String.match?(~r/^local|^127.0.0.1/) {:local, _} -> true
_ -> false
end
remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled]) remote_ip_not_found =
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
localhost_or_socket and remote_ip_disabled localhost_or_socket and remote_ip_not_found
end end
@inspect_bucket_not_found {:error, :not_found} @inspect_bucket_not_found {:error, :not_found}

View file

@ -7,6 +7,8 @@ defmodule Pleroma.Plugs.RemoteIp do
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
""" """
import Plug.Conn
@behaviour Plug @behaviour Plug
@headers ~w[ @headers ~w[
@ -26,11 +28,12 @@ defmodule Pleroma.Plugs.RemoteIp do
def init(_), do: nil def init(_), do: nil
def call(conn, _) do def call(%{remote_ip: original_remote_ip} = conn, _) do
config = Pleroma.Config.get(__MODULE__, []) config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do if Keyword.get(config, :enabled, false) do
RemoteIp.call(conn, remote_ip_opts(config)) %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
else else
conn conn
end end

View file

@ -21,6 +21,9 @@ def call(conn, _) do
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
defp accepts_html?(conn) do defp accepts_html?(conn) do
conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html") case get_req_header(conn, "accept") do
[accept | _] -> String.contains?(accept, "text/html")
_ -> false
end
end end
end end

View file

@ -14,9 +14,14 @@ defmodule Pleroma.Plugs.UploadedMedia do
# no slashes # no slashes
@path "media" @path "media"
@default_cache_control_header "public, max-age=1209600"
def init(_opts) do def init(_opts) do
static_plug_opts = static_plug_opts =
[] [
headers: %{"cache-control" => @default_cache_control_header},
cache_control_for_etags: @default_cache_control_header
]
|> Keyword.put(:from, "__unconfigured_media_plug") |> Keyword.put(:from, "__unconfigured_media_plug")
|> Keyword.put(:at, "/__unconfigured_media_plug") |> Keyword.put(:at, "/__unconfigured_media_plug")
|> Plug.Static.init() |> Plug.Static.init()

View file

@ -7,7 +7,7 @@ defmodule Pleroma.ReverseProxy do
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match if-range range) ~w(if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control) @resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++ @keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range) ++ ~w(content-type content-disposition content-encoding content-range) ++
~w(accept-ranges vary) ~w(accept-ranges vary)
@ -34,9 +34,6 @@ defmodule Pleroma.ReverseProxy do
* request: `#{inspect(@keep_req_headers)}` * request: `#{inspect(@keep_req_headers)}`
* response: `#{inspect(@keep_resp_headers)}` * response: `#{inspect(@keep_resp_headers)}`
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
set to `#{inspect(@default_cache_control_header)}`.
Options: Options:
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP * `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
@ -297,16 +294,17 @@ defp build_resp_headers(headers, opts) do
defp build_resp_cache_headers(headers, _opts) do defp build_resp_cache_headers(headers, _opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end) has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
has_cache_control? = List.keymember?(headers, "cache-control", 0)
cond do cond do
has_cache? && has_cache_control? ->
headers
has_cache? -> has_cache? ->
# There's caching header present but no cache-control -- we need to explicitely override it # There's caching header present but no cache-control -- we need to set our own
# to public as Plug defaults to "max-age=0, private, must-revalidate" # as Plug defaults to "max-age=0, private, must-revalidate"
List.keystore(headers, "cache-control", 0, {"cache-control", "public"}) List.keystore(
headers,
"cache-control",
0,
{"cache-control", @default_cache_control_header}
)
true -> true ->
List.keystore( List.keystore(

View file

@ -9,7 +9,8 @@ defmodule Pleroma.ThreadMute do
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
require Ecto.Query import Ecto.Changeset
import Ecto.Query
schema "thread_mutes" do schema "thread_mutes" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
@ -18,19 +19,44 @@ defmodule Pleroma.ThreadMute do
def changeset(mute, params \\ %{}) do def changeset(mute, params \\ %{}) do
mute mute
|> Ecto.Changeset.cast(params, [:user_id, :context]) |> cast(params, [:user_id, :context])
|> Ecto.Changeset.foreign_key_constraint(:user_id) |> foreign_key_constraint(:user_id)
|> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index) |> unique_constraint(:user_id, name: :unique_index)
end end
def query(user_id, context) do def query(user_id, context) do
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id) user_binary_id = User.binary_id(user_id)
ThreadMute ThreadMute
|> Ecto.Query.where(user_id: ^user_id) |> where(user_id: ^user_binary_id)
|> Ecto.Query.where(context: ^context) |> where(context: ^context)
end end
def muters_query(context) do
ThreadMute
|> join(:inner, [tm], u in assoc(tm, :user))
|> where([tm], tm.context == ^context)
|> select([tm, u], u.ap_id)
end
def muter_ap_ids(context, ap_ids \\ nil)
# Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
def muter_ap_ids(context, _ap_ids) when is_nil(context), do: []
def muter_ap_ids(context, ap_ids) do
context
|> muters_query()
|> maybe_filter_on_ap_id(ap_ids)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [tm, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def add_mute(user_id, context) do def add_mute(user_id, context) do
%ThreadMute{} %ThreadMute{}
|> changeset(%{user_id: user_id, context: context}) |> changeset(%{user_id: user_id, context: context})
@ -42,8 +68,8 @@ def remove_mute(user_id, context) do
|> Repo.delete_all() |> Repo.delete_all()
end end
def check_muted(user_id, context) do def exists?(user_id, context) do
query(user_id, context) query(user_id, context)
|> Repo.all() |> Repo.exists?()
end end
end end

View file

@ -16,6 +16,7 @@ defmodule Pleroma.User do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.HTML
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -149,22 +150,26 @@ defmodule Pleroma.User do
{outgoing_relation, outgoing_relation_target}, {outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source} {incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do ]} <- @user_relationships_config do
# Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc. # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
# :notification_muter_mutes, :subscribee_subscriptions
has_many(outgoing_relation, UserRelationship, has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id, foreign_key: :source_id,
where: [relationship_type: relationship_type] where: [relationship_type: relationship_type]
) )
# Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc. # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
# :notification_mutee_mutes, :subscriber_subscriptions
has_many(incoming_relation, UserRelationship, has_many(incoming_relation, UserRelationship,
foreign_key: :target_id, foreign_key: :target_id,
where: [relationship_type: relationship_type] where: [relationship_type: relationship_type]
) )
# Definitions of `has_many :blocked_users`, `has_many :muted_users` etc. # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
# :notification_muted_users, :subscriber_users
has_many(outgoing_relation_target, through: [outgoing_relation, :target]) has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many :blocker_users`, `has_many :muter_users` etc. # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
# :notification_muter_users, :subscribee_users
has_many(incoming_relation_source, through: [incoming_relation, :source]) has_many(incoming_relation_source, through: [incoming_relation, :source])
end end
@ -184,7 +189,9 @@ defmodule Pleroma.User do
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do @user_relationships_config do
# Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc. # `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
# `def subscriber_users/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target)) target_users_query = assoc(user, unquote(outgoing_relation_target))
@ -195,7 +202,8 @@ def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated?
end end
end end
# Definitions of `blocked_users/1`, `muted_users/1`, etc. # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
# `def notification_muted_users/2`, `def subscriber_users/2`
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__ __MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@ -205,7 +213,8 @@ def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
|> Repo.all() |> Repo.all()
end end
# Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc. # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
# `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__ __MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@ -217,6 +226,24 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \
end end
end end
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account" @doc "Returns status account"
@spec account_status(User.t()) :: account_status() @spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{deactivated: true}), do: :deactivated
@ -236,7 +263,18 @@ def visible_for?(user, for_user \\ nil)
def visible_for?(%User{invisible: true}, _), do: false def visible_for?(%User{invisible: true}, _), do: false
def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
def visible_for?(%User{local: local} = user, nil) do
cfg_key =
if local,
do: :local,
else: :remote
if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
do: false,
else: account_status(user) == :active
end
def visible_for?(%User{} = user, for_user) do def visible_for?(%User{} = user, for_user) do
account_status(user) == :active || superuser?(for_user) account_status(user) == :active || superuser?(for_user)
@ -280,24 +318,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def follow_state(%User{} = user, %User{} = target) do
case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
# Ideally this would be nil, but then Cachex does not commit the value
_ -> false
end
end
def get_cached_follow_state(user, target) do
key = "follow_state:#{user.ap_id}|#{target.ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
end
@spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true) from(u in query, where: u.deactivated != ^true)
@ -416,9 +436,55 @@ def update_changeset(struct, params \\ %{}) do
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> put_fields()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> validate_fields(false) |> validate_fields(false)
end end
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
|> put_change(:fields, fields)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
_ -> changeset
end
else
changeset
end
end
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
{:ok, object.data}
end
end
def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@ -462,6 +528,27 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|> validate_fields(remote?) |> validate_fields(remote?)
end end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
|> cast(params, [:email])
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
end
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
if params["password"] do
reset_password(user, changeset, params)
else
User.update_and_set_cache(changeset)
end
end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
struct struct
|> cast(params, [:password, :password_confirmation]) |> cast(params, [:password, :password_confirmation])
@ -472,10 +559,14 @@ def password_update_changeset(struct, params) do
end end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{id: user_id} = user, data) do def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
def reset_password(%User{id: user_id} = user, struct, params) do
multi = multi =
Multi.new() Multi.new()
|> Multi.update(:user, password_update_changeset(user, data)) |> Multi.update(:user, password_update_changeset(struct, params))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
@ -530,7 +621,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
end end
def maybe_validate_required_email(changeset, true), do: changeset def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
def maybe_validate_required_email(changeset, _) do
if Pleroma.Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
end
end
defp put_ap_id(changeset) do defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)}) ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
@ -673,7 +771,14 @@ def unfollow(%User{} = follower, %User{} = followed) do
def get_follow_state(%User{} = follower, %User{} = following) do def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following) following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
end
def get_follow_state(
%User{} = follower,
%User{} = following,
following_relationship
) do
case {following_relationship, following.local} do case {following_relationship, following.local} do
{nil, false} -> {nil, false} ->
case Utils.fetch_latest_follow(follower, following) do case Utils.fetch_latest_follow(follower, following) do
@ -832,10 +937,6 @@ def get_or_fetch_by_nickname(nickname) do
_e -> _e ->
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
fetch_initial_posts(user)
end
{:ok, user} {:ok, user}
else else
_e -> {:error, "not found " <> nickname} _e -> {:error, "not found " <> nickname}
@ -843,11 +944,6 @@ def get_or_fetch_by_nickname(nickname) do
end end
end end
@doc "Fetch some posts when the user has just been federated with"
def fetch_initial_posts(user) do
BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, deactivated: false}) User.Query.build(%{followers: user, deactivated: false})
@ -1215,13 +1311,15 @@ def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
end end
@doc """ @doc """
Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
""" """
@spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relations_ap_ids(_, []), do: %{} def outgoing_relationships_ap_ids(_user, []), do: %{}
def outgoing_relations_ap_ids(%User{} = user, relationship_types) def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do when is_list(relationship_types) do
db_result = db_result =
user user
@ -1240,6 +1338,30 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types)
) )
end end
def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
when is_list(relationship_types) do
user
|> assoc(:incoming_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :source))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> maybe_filter_on_ap_id(ap_ids)
|> select([user_rel, u], u.ap_id)
|> distinct(true)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [user_rel, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def deactivate_async(user, status \\ true) do def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end end
@ -1313,16 +1435,6 @@ def perform(:delete, %User{} = user) do
Repo.delete(user) Repo.delete(user)
end end
def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
# Insert all the posts in reverse order, so they're in the right order on the timeline
user.source_data["outbox"]
|> Utils.fetch_ordered_collection(pages)
|> Enum.reverse()
|> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status) def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()} @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
@ -1451,18 +1563,7 @@ def get_or_fetch_by_ap_id(ap_id) do
if !is_nil(user) and !needs_update?(user) do if !is_nil(user) and !needs_update?(user) do
{:ok, user} {:ok, user}
else else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled) fetch_by_ap_id(ap_id)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do
with {:ok, %User{} = user} <- resp do
fetch_initial_posts(user)
end
end
resp
end end
end end
@ -1671,8 +1772,12 @@ def all_superusers do
|> Repo.all() |> Repo.all()
end end
def muting_reblogs?(%User{} = user, %User{} = target) do
UserRelationship.reblog_mute_exists?(user, target)
end
def showing_reblogs?(%User{} = user, %User{} = target) do def showing_reblogs?(%User{} = user, %User{} = target) do
not UserRelationship.reblog_mute_exists?(user, target) not muting_reblogs?(user, target)
end end
@doc """ @doc """
@ -1878,6 +1983,17 @@ def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields def fields(%{fields: fields}), do: fields
def sanitized_fields(%User{} = user) do
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
end
def validate_fields(changeset, remote? \\ false) do def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0) limit = Pleroma.Config.get([:instance, limit_name], 0)
@ -2055,4 +2171,27 @@ def set_invisible(user, invisible) do
|> validate_required([:invisible]) |> validate_required([:invisible])
|> update_and_set_cache() |> update_and_set_cache()
end end
def sanitize_html(%User{} = user) do
sanitize_html(user, nil)
end
# User data that mastodon isn't filtering (treated as plaintext):
# - field name
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
user
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Pleroma.FollowingRelationship
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
@ -21,19 +22,26 @@ defmodule Pleroma.UserRelationship do
end end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do
# Definitions of `create_block/2`, `create_mute/2` etc. # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
# `def create_notification_mute/2`, `def create_inverse_subscription/2`
def unquote(:"create_#{relationship_type}")(source, target), def unquote(:"create_#{relationship_type}")(source, target),
do: create(unquote(relationship_type), source, target) do: create(unquote(relationship_type), source, target)
# Definitions of `delete_block/2`, `delete_mute/2` etc. # `def delete_block/2`, `def delete_mute/2`, `def delete_reblog_mute/2`,
# `def delete_notification_mute/2`, `def delete_inverse_subscription/2`
def unquote(:"delete_#{relationship_type}")(source, target), def unquote(:"delete_#{relationship_type}")(source, target),
do: delete(unquote(relationship_type), source, target) do: delete(unquote(relationship_type), source, target)
# Definitions of `block_exists?/2`, `mute_exists?/2` etc. # `def block_exists?/2`, `def mute_exists?/2`, `def reblog_mute_exists?/2`,
# `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2`
def unquote(:"#{relationship_type}_exists?")(source, target), def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target) do: exists?(unquote(relationship_type), source, target)
end end
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__()
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship user_relationship
|> cast(params, [:relationship_type, :source_id, :target_id]) |> cast(params, [:relationship_type, :source_id, :target_id])
@ -72,6 +80,73 @@ def delete(relationship_type, %User{} = source, %User{} = target) do
end end
end end
def dictionary(
source_users,
target_users,
source_to_target_rel_types \\ nil,
target_to_source_rel_types \\ nil
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end
source_to_target_rel_types =
Enum.map(source_to_target_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
target_to_source_rel_types =
Enum.map(target_to_source_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
__MODULE__
|> where(
fragment(
"(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?)) OR \
(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?))",
^source_user_ids,
^target_user_ids,
^source_to_target_rel_types,
^target_user_ids,
^source_user_ids,
^target_to_source_rel_types
)
)
|> select([ur], [ur.relationship_type, ur.source_id, ur.target_id])
|> Repo.all()
end
def exists?(dictionary, rel_type, source, target, func) do
cond do
is_nil(source) or is_nil(target) ->
false
dictionary ->
[rel_type, source.id, target.id] in dictionary
true ->
func.(source, target)
end
end
@doc ":relationships option for StatusView / AccountView / NotificationView"
def view_relationships_option(nil = _reading_user, _actors) do
%{user_relationships: [], following_relationships: []}
end
def view_relationships_option(%User{} = reading_user, actors) do
user_relationships =
UserRelationship.dictionary(
[reading_user],
actors,
[:block, :mute, :notification_mute, :reblog_mute],
[:block, :inverse_subscription]
)
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
%{user_relationships: user_relationships, following_relationships: following_relationships}
end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
changeset changeset
|> validate_change(:target_id, fn _, target_id -> |> validate_change(:target_id, fn _, target_id ->

View file

@ -503,8 +503,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
defp do_follow(follower, followed, activity_id, local) do defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id), with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity} {:ok, activity}
else else
{:error, error} -> Repo.rollback(error) {:error, error} -> Repo.rollback(error)
@ -584,6 +583,16 @@ defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options)
end end
end end
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
activity =
ap_id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Delete")
|> Repo.one()
{:ok, activity}
end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) :: @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@ -1230,17 +1239,17 @@ defp maybe_order(query, _), do: query
defp fetch_activities_query_ap_ids_ops(opts) do defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts["muting_user"] source_user = opts["muting_user"]
ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relations = ap_id_relationships =
ap_id_relations ++ ap_id_relationships ++
if opts["blocking_user"] && opts["blocking_user"] == source_user do if opts["blocking_user"] && opts["blocking_user"] == source_user do
[:block] [:block]
else else
[] []
end end
preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
@ -18,23 +19,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
require Logger require Logger
action_fallback(:errors) action_fallback(:errors)
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
plug(FederatingPlug when action in @federating_only_actions)
plug(
EnsureAuthenticatedPlug,
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
)
plug(
EnsureAuthenticatedPlug
when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
)
plug( plug(
Pleroma.Plugs.Cache, Pleroma.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2] [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object] when action in [:activity, :object]
) )
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox]) plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay]) plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do if Pleroma.Config.get([:instance, :allow_relay]) do
conn conn
else else
@ -127,11 +142,13 @@ defp set_cache_ttl_for(conn, entity) do
end end
# GET /relay/following # GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do def relay_following(conn, _params) do
conn with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|> put_resp_content_type("application/activity+json") conn
|> put_view(UserView) |> put_resp_content_type("application/activity+json")
|> render("following.json", %{user: Relay.get_actor()}) |> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end
end end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -164,11 +181,13 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
end end
# GET /relay/followers # GET /relay/followers
def followers(%{assigns: %{relay: true}} = conn, _params) do def relay_followers(conn, _params) do
conn with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|> put_resp_content_type("application/activity+json") conn
|> put_view(UserView) |> put_resp_content_type("application/activity+json")
|> render("followers.json", %{user: Relay.get_actor()}) |> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -200,13 +219,16 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
end end
end end
def outbox(conn, %{"nickname" => nickname, "page" => page?} = params) def outbox(
%{assigns: %{user: for_user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
activities = activities =
if params["max_id"] do if params["max_id"] do
ActivityPub.fetch_user_activities(user, nil, %{ ActivityPub.fetch_user_activities(user, for_user, %{
"max_id" => params["max_id"], "max_id" => params["max_id"],
# This is a hack because postgres generates inefficient queries when filtering by # This is a hack because postgres generates inefficient queries when filtering by
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
@ -214,7 +236,7 @@ def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
"limit" => 10 "limit" => 10
}) })
else else
ActivityPub.fetch_user_activities(user, nil, %{ ActivityPub.fetch_user_activities(user, for_user, %{
"limit" => 10, "limit" => 10,
"include_poll_votes" => true "include_poll_votes" => true
}) })
@ -255,8 +277,16 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
json(conn, "ok") json(conn, "ok")
end end
# only accept relayed Creates # POST /relay/inbox -or- POST /internal/fetch/inbox
def inbox(conn, %{"type" => "Create"} = params) do def inbox(conn, params) do
if params["type"] == "Create" && FederatingPlug.federating?() do
post_inbox_relayed_create(conn, params)
else
post_inbox_fallback(conn, params)
end
end
defp post_inbox_relayed_create(conn, params) do
Logger.debug( Logger.debug(
"Signature missing or not from author, relayed Create message, fetching object from source" "Signature missing or not from author, relayed Create message, fetching object from source"
) )
@ -266,10 +296,11 @@ def inbox(conn, %{"type" => "Create"} = params) do
json(conn, "ok") json(conn, "ok")
end end
def inbox(conn, params) do defp post_inbox_fallback(conn, params) do
headers = Enum.into(conn.req_headers, %{}) headers = Enum.into(conn.req_headers, %{})
if String.contains?(headers["signature"], params["actor"]) do if headers["signature"] && params["actor"] &&
String.contains?(headers["signature"], params["actor"]) do
Logger.debug( Logger.debug(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!" "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
) )
@ -277,7 +308,9 @@ def inbox(conn, params) do
Logger.debug(inspect(conn.req_headers)) Logger.debug(inspect(conn.req_headers))
end end
json(conn, dgettext("errors", "error")) conn
|> put_status(:bad_request)
|> json(dgettext("errors", "error"))
end end
defp represent_service_actor(%User{} = user, conn) do defp represent_service_actor(%User{} = user, conn) do
@ -311,10 +344,8 @@ def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
|> render("user.json", %{user: user}) |> render("user.json", %{user: user})
end end
def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox( def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params %{"nickname" => nickname, "page" => page?} = params
) )
when page? in [true, "true"] do when page? in [true, "true"] do
@ -337,7 +368,7 @@ def read_inbox(
}) })
end end
def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{ def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname "nickname" => nickname
}) do }) do
with {:ok, user} <- User.ensure_keys_present(user) do with {:ok, user} <- User.ensure_keys_present(user) do
@ -348,15 +379,7 @@ def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
end end
end end
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
conn
|> put_status(:forbidden)
|> json(err)
end
def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
"nickname" => nickname "nickname" => nickname
}) do }) do
err = err =
@ -370,7 +393,7 @@ def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
|> json(err) |> json(err)
end end
def handle_user_activity(user, %{"type" => "Create"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
object = object =
params["object"] params["object"]
|> Map.merge(Map.take(params, ["to", "cc"])) |> Map.merge(Map.take(params, ["to", "cc"]))
@ -386,7 +409,7 @@ def handle_user_activity(user, %{"type" => "Create"} = params) do
}) })
end end
def handle_user_activity(user, %{"type" => "Delete"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"], true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
@ -396,7 +419,7 @@ def handle_user_activity(user, %{"type" => "Delete"} = params) do
end end
end end
def handle_user_activity(user, %{"type" => "Like"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do {:ok, activity, _object} <- ActivityPub.like(user, object) do
{:ok, activity} {:ok, activity}
@ -405,7 +428,7 @@ def handle_user_activity(user, %{"type" => "Like"} = params) do
end end
end end
def handle_user_activity(_, _) do defp handle_user_activity(_, _) do
{:error, dgettext("errors", "Unhandled activity type")} {:error, dgettext("errors", "Unhandled activity type")}
end end
@ -434,7 +457,7 @@ def update_outbox(
end end
end end
def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
err = err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname, nickname: nickname,
@ -446,13 +469,13 @@ def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} =
|> json(err) |> json(err)
end end
def errors(conn, {:error, :not_found}) do defp errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(dgettext("errors", "Not found")) |> json(dgettext("errors", "Not found"))
end end
def errors(conn, _e) do defp errors(conn, _e) do
conn conn
|> put_status(:internal_server_error) |> put_status(:internal_server_error)
|> json(dgettext("errors", "error")) |> json(dgettext("errors", "error"))
@ -492,7 +515,7 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
- HTTP Code: 201 Created - HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field - HTTP Body: ActivityPub object to be inserted into another's `attachment` field
""" """
def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- with {:ok, object} <-
ActivityPub.upload( ActivityPub.upload(
file, file,

View file

@ -60,15 +60,28 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
def publish(_), do: {:error, "Not implemented"} def publish(_), do: {:error, "Not implemented"}
@spec list() :: {:ok, [String.t()]} | {:error, any()} @spec list(boolean()) :: {:ok, [String.t()]} | {:error, any()}
def list do def list(with_not_accepted \\ false) do
with %User{} = user <- get_actor() do with %User{} = user <- get_actor() do
list = accepted =
user user
|> User.following() |> User.following()
|> Enum.map(fn entry -> URI.parse(entry).host end) |> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq() |> Enum.uniq()
list =
if with_not_accepted do
without_accept =
user
|> Pleroma.Activity.following_requests_for_actor()
|> Enum.map(fn a -> URI.parse(a.data["object"]).host <> " (no Accept received)" end)
|> Enum.uniq()
accepted ++ without_accept
else
accepted
end
{:ok, list} {:ok, list}
else else
error -> format_error(error) error -> format_error(error)

View file

@ -1108,13 +1108,11 @@ def add_hashtags(object) do
end end
def add_mention_tags(object) do def add_mention_tags(object) do
mentions = {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
object potential_receivers = enabled_receivers ++ disabled_receivers
|> Utils.get_notified_from_object() mentions = Enum.map(potential_receivers, &build_mention_tag/1)
|> Enum.map(&build_mention_tag/1)
tags = object["tag"] || [] tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions) Map.put(object, "tag", tags ++ mentions)
end end

View file

@ -440,22 +440,19 @@ def update_follow_state_for_all(
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([]) |> Repo.update_all([])
User.set_follow_state_cache(actor, object, state)
activity = Activity.get_by_id(activity.id) activity = Activity.get_by_id(activity.id)
{:ok, activity} {:ok, activity}
end end
def update_follow_state( def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity, %Activity{} = activity,
state state
) do ) do
new_data = Map.put(activity.data, "state", state) new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data) changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do with {:ok, activity} <- Repo.update(changeset) do
User.set_follow_state_cache(actor, object, state)
{:ok, activity} {:ok, activity}
end end
end end
@ -784,45 +781,6 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do
defp build_flag_object(_), do: [] defp build_flag_object(_), do: []
@doc """
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
the first one to `pages_left` pages.
If the amount of pages is higher than the collection has, it returns whatever was there.
"""
def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do
"OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page,
# just call the same function on the page address
fetch_ordered_collection(collection["first"], pages_left)
"OrderedCollectionPage" ->
if pages_left > 0 do
# There are still more pages
if Map.has_key?(collection, "next") do
# There are still more pages, go deeper saving what we have into the accumulator
fetch_ordered_collection(
collection["next"],
pages_left - 1,
acc ++ collection["orderedItems"]
)
else
# No more pages left, just return whatever we already have
acc ++ collection["orderedItems"]
end
else
# Got the amount of pages needed, add them all to the accumulator
acc ++ collection["orderedItems"]
end
_ ->
{:error, "Not an OrderedCollection or OrderedCollectionPage"}
end
end
end
#### Report-related helpers #### Report-related helpers
def get_reports(params, page, page_size) do def get_reports(params, page, page_size) do
params = params =

View file

@ -73,6 +73,7 @@ def render("user.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
@ -81,12 +82,6 @@ def render("user.json", %{user: user}) do
fields = fields =
user user
|> User.fields() |> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
|> Enum.map(&Map.put(&1, "type", "PropertyValue")) |> Enum.map(&Map.put(&1, "type", "PropertyValue"))
%{ %{

View file

@ -44,6 +44,7 @@ def is_direct?(activity) do
def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false def is_list?(_), do: false
@spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean()
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
@ -55,14 +56,21 @@ def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(activity, nil) do def visible_for_user?(%{local: local} = activity, nil) do
is_public?(activity) cfg_key =
if local,
do: :local,
else: :remote
if Pleroma.Config.get([:restrict_unauthenticated, :activities, cfg_key]),
do: false,
else: is_public?(activity)
end end
def visible_for_user?(activity, user) do def visible_for_user?(activity, user) do
x = [user.ap_id | User.following(user)] x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) is_public?(activity) || Enum.any?(x, &(&1 in y))
end end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true} %{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get] when action in [:list_users, :user_show, :right_get, :show_user_credentials]
) )
plug( plug(
@ -54,7 +54,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:tag_users, :tag_users,
:untag_users, :untag_users,
:right_add, :right_add,
:right_delete :right_delete,
:update_user_credentials
] ]
) )
@ -658,6 +659,52 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
json_response(conn, :no_content, "") json_response(conn, :no_content, "")
end end
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin})
else
_ -> {:error, :not_found}
end
end
@doc "Updates a given user"
def update_user_credentials(
%{assigns: %{user: admin}} = conn,
%{"nickname" => nickname} = params
) do
with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
{:ok, _user} <-
User.update_as_admin(user, params) do
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "updated_users"
})
if params["password"] do
User.force_password_reset_async(user)
end
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "force_password_reset"
})
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
end
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
@ -745,14 +792,14 @@ def report_notes_delete(%{assigns: %{user: user}} = conn, %{
end end
end end
def list_statuses(%{assigns: %{user: admin}} = conn, params) do def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
godmode = params["godmode"] == "true" || params["godmode"] == true godmode = params["godmode"] == "true" || params["godmode"] == true
local_only = params["local_only"] == "true" || params["local_only"] == true local_only = params["local_only"] == "true" || params["local_only"] == true
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
activities = activities =
ActivityPub.fetch_statuses(admin, %{ ActivityPub.fetch_statuses(nil, %{
"godmode" => godmode, "godmode" => godmode,
"local_only" => local_only, "local_only" => local_only,
"limit" => page_size, "limit" => page_size,
@ -834,7 +881,7 @@ def config_show(conn, _params) do
configs = ConfigDB.get_all_as_keyword() configs = ConfigDB.get_all_as_keyword()
merged = merged =
Config.Holder.config() Config.Holder.default_config()
|> ConfigDB.merge(configs) |> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} -> |> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} -> Enum.map(values, fn {key, value} ->

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.AdminAPI.AccountView do defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.HTML
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@ -24,9 +23,47 @@ def render("index.json", %{users: users}) do
} }
end end
def render("credentials.json", %{user: user, for: for_user}) do
user = User.sanitize_html(user, User.html_filter_policy(for_user))
avatar = User.avatar_url(user) |> MediaProxy.url()
banner = User.banner_url(user) |> MediaProxy.url()
background = image_url(user.background) |> MediaProxy.url()
user
|> Map.take([
:id,
:bio,
:email,
:fields,
:name,
:nickname,
:locked,
:no_rich_text,
:default_scope,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_favorites,
:allow_following_move,
:show_role,
:skip_thread_containment,
:pleroma_settings_store,
:raw_fields,
:discoverable,
:actor_type
])
|> Map.merge(%{
"avatar" => avatar,
"banner" => banner,
"background" => background
})
end
def render("show.json", %{user: user}) do def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url() avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = HTML.strip_tags(user.name || user.nickname) display_name = Pleroma.HTML.strip_tags(user.name || user.nickname)
user = User.sanitize_html(user, FastSanitize.Sanitizer.StripTags)
%{ %{
"id" => user.id, "id" => user.id,
@ -104,4 +141,7 @@ defp parse_error(errors) do
"" ""
end end
end end
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end end

View file

@ -358,7 +358,7 @@ def remove_mute(user, activity) do
def thread_muted?(%{id: nil} = _user, _activity), do: false def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do def thread_muted?(user, activity) do
ThreadMute.check_muted(user.id, activity.data["context"]) != [] ThreadMute.exists?(user.id, activity.data["context"])
end end
def report(user, %{"account_id" => account_id} = data) do def report(user, %{"account_id" => account_id} = data) do

View file

@ -331,7 +331,7 @@ def format_input(text, "text/html", options) do
def format_input(text, "text/markdown", options) do def format_input(text, "text/markdown", options) do
text text
|> Formatter.mentions_escape(options) |> Formatter.mentions_escape(options)
|> Earmark.as_html!() |> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer})
|> Formatter.linkify(options) |> Formatter.linkify(options)
|> Formatter.html_escape("text/html") |> Formatter.html_escape("text/html")
end end
@ -591,7 +591,7 @@ def validate_character_limit(full_payload, _attachments) do
limit = Pleroma.Config.get([:instance, :limit]) limit = Pleroma.Config.get([:instance, :limit])
length = String.length(full_payload) length = String.length(full_payload)
if length < limit do if length <= limit do
:ok :ok
else else
{:error, dgettext("errors", "The status is over the character limit")} {:error, dgettext("errors", "The status is over the character limit")}

View file

@ -34,7 +34,12 @@ defp param_to_integer(val, default) when is_binary(val) do
defp param_to_integer(_, default), do: default defp param_to_integer(_, default), do: default
def add_link_headers(conn, activities, extra_params \\ %{}) do def add_link_headers(conn, activities, extra_params \\ %{})
def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params),
do: conn
def add_link_headers(conn, activities, extra_params) do
case List.last(activities) do case List.last(activities) do
%{id: max_id} -> %{id: max_id} ->
params = params =
@ -87,7 +92,8 @@ def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity") render_error(conn, :not_implemented, "Can't display this activity")
end end
@spec put_in_if_exist(map(), atom() | String.t(), any) :: map() @spec put_if_exist(map(), atom() | String.t(), any) :: map()
def put_in_if_exist(map, _key, nil), do: map def put_if_exist(map, _key, nil), do: map
def put_in_if_exist(map, key, value), do: put_in(map, key, value)
def put_if_exist(map, key, value), do: Map.put(map, key, value)
end end

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Plugs.HTTPSecurityPlug)
plug(Pleroma.Plugs.UploadedMedia) plug(Pleroma.Plugs.UploadedMedia)
@static_cache_control "public max-age=86400 must-revalidate" @static_cache_control "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well

View file

@ -9,18 +9,18 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3] import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
def feed(conn, %{"tag" => raw_tag} = params) do def feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag) {format, tag} = parse_tag(raw_tag)
activities = activities =
%{"type" => ["Create"], "tag" => tag} %{"type" => ["Create"], "tag" => tag}
|> put_in_if_exist("max_id", params["max_id"]) |> put_if_exist("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/#{format}+xml")
|> put_view(FeedView) |> put_view(FeedView)
|> render("tag.#{format}", |> render("tag.#{format}",
activities: activities, activities: activities,

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.Feed.UserController do
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3] import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
@ -25,7 +25,12 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname
def feed_redirect(%{assigns: %{format: format}} = conn, _params) def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :user) with %{halted: false} = conn <-
Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
) do
ActivityPubController.call(conn, :user)
end
end end
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname}) do
@ -35,19 +40,28 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
end end
def feed(conn, %{"nickname" => nickname} = params) do def feed(conn, %{"nickname" => nickname} = params) do
format = get_format(conn)
format =
if format in ["rss", "atom"] do
format
else
"atom"
end
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities = activities =
%{ %{
"type" => ["Create"], "type" => ["Create"],
"actor_id" => user.ap_id "actor_id" => user.ap_id
} }
|> put_in_if_exist("max_id", params["max_id"]) |> put_if_exist("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/#{format}+xml")
|> put_view(FeedView) |> put_view(FeedView)
|> render("user.xml", |> render("user.#{format}",
user: user, user: user,
activities: activities, activities: activities,
feed_config: Pleroma.Config.get([:feed]) feed_config: Pleroma.Config.get([:feed])

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
alias Pleroma.Emoji
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.User alias Pleroma.User
@ -60,14 +59,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug( plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :create when action not in [:create, :show, :statuses]
) )
@relations [:follow, :unfollow] @relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations) plug(
plug(RateLimiter, [name: :relations_actions] when action in @relations) RateLimiter,
[name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create) plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account) plug(:assign_account_by_id when action in @needs_account)
@ -76,7 +79,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts" @doc "POST /api/v1/accounts"
def create( def create(
%{assigns: %{app: app}} = conn, %{assigns: %{app: app}} = conn,
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params %{"username" => nickname, "password" => _, "agreement" => true} = params
) do ) do
params = params =
params params
@ -93,7 +96,8 @@ def create(
|> Map.put("bio", params["bio"] || "") |> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"]) |> Map.put("confirm", params["password"])
with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), with :ok <- validate_email_param(params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{ json(conn, %{
token_type: "Bearer", token_type: "Bearer",
@ -114,6 +118,15 @@ def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials") render_error(conn, :forbidden, "Invalid credentials")
end end
defp validate_email_param(%{"email" => _}), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
true -> {:error, %{"error" => "Missing parameters"}}
_ -> :ok
end
end
@doc "GET /api/v1/accounts/verify_credentials" @doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id) chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
@ -130,17 +143,6 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
user = original_user user = original_user
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
user_params = user_params =
[ [
:no_rich_text, :no_rich_text,
@ -159,46 +161,20 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end) end)
|> add_if_present(params, "display_name", :name) |> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) |> add_if_present(params, "note", :bio)
|> add_if_present(params, "avatar", :avatar, fn value -> |> add_if_present(params, "avatar", :avatar)
with %Plug.Upload{} <- value, |> add_if_present(params, "header", :banner)
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do |> add_if_present(params, "pleroma_background_image", :background)
{:ok, object.data} |> add_if_present(
end params,
end) "fields_attributes",
|> add_if_present(params, "header", :banner, fn value -> :raw_fields,
with %Plug.Upload{} <- value, &{:ok, normalize_fields_attributes(&1)}
{:ok, object} <- ActivityPub.upload(value, type: :banner) do )
{:ok, object.data} |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
end
end)
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :background) do
{:ok, object.data}
end
end)
|> add_if_present(params, "fields_attributes", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields}
end)
|> add_if_present(params, "fields_attributes", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.pleroma_settings_store, value)}
end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "actor_type", :actor_type) |> add_if_present(params, "actor_type", :actor_type)
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
user_emojis =
user
|> Map.get(:emoji, [])
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
user_params = Map.put(user_params, :emoji, user_emojis)
changeset = User.update_changeset(user, user_params) changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do with {:ok, user} <- User.update_and_set_cache(changeset) do
@ -249,7 +225,8 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
@doc "GET /api/v1/accounts/:id/statuses" @doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user),
true <- User.visible_for?(user, reading_user) do
params = params =
params params
|> Map.put("tag", params["tagged"]) |> Map.put("tag", params["tagged"])
@ -261,6 +238,8 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|> add_link_headers(activities) |> add_link_headers(activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", activities: activities, for: reading_user, as: :activity) |> render("index.json", activities: activities, for: reading_user, as: :activity)
else
_e -> render_error(conn, :not_found, "Can't find user")
end end
end end

View file

@ -86,6 +86,6 @@ defp local_mastodon_root_path(conn) do
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()} @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
defp get_or_make_app do defp get_or_make_app do
%{client_name: @local_mastodon_name, redirect_uris: "."} %{client_name: @local_mastodon_name, redirect_uris: "."}
|> App.get_or_make(["read", "write", "follow", "push"]) |> App.get_or_make(["read", "write", "follow", "push", "admin"])
end end
end end

View file

@ -76,7 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
) )
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :show])
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :public)
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
@ -75,17 +75,30 @@ def direct(%{assigns: %{user: user}} = conn, params) do
def public(%{assigns: %{user: user}} = conn, params) do def public(%{assigns: %{user: user}} = conn, params) do
local_only = truthy_param?(params["local"]) local_only = truthy_param?(params["local"])
activities = cfg_key =
params if local_only do
|> Map.put("type", ["Create", "Announce"]) :local
|> Map.put("local_only", local_only) else
|> Map.put("blocking_user", user) :federated
|> Map.put("muting_user", user) end
|> ActivityPub.fetch_public_activities()
conn restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity) if not (restrict? and is_nil(user)) do
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities()
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
else
render_error(conn, :unauthorized, "authorization required for timeline view")
end
end end
def hashtag_fetching(params, user, local_only) do def hashtag_fetching(params, user, local_only) do

View file

@ -55,6 +55,7 @@ def get_notifications(user, params \\ %{}) do
user user
|> Notification.for_user_query(options) |> Notification.for_user_query(options)
|> restrict(:include_types, options)
|> restrict(:exclude_types, options) |> restrict(:exclude_types, options)
|> restrict(:account_ap_id, options) |> restrict(:account_ap_id, options)
|> Pagination.fetch_paginated(params) |> Pagination.fetch_paginated(params)
@ -69,10 +70,10 @@ def get_scheduled_activities(user, params \\ %{}) do
defp cast_params(params) do defp cast_params(params) do
param_types = %{ param_types = %{
exclude_types: {:array, :string}, exclude_types: {:array, :string},
include_types: {:array, :string},
exclude_visibilities: {:array, :string}, exclude_visibilities: {:array, :string},
reblogs: :boolean, reblogs: :boolean,
with_muted: :boolean, with_muted: :boolean,
with_move: :boolean,
account_ap_id: :string account_ap_id: :string
} }
@ -80,14 +81,16 @@ defp cast_params(params) do
changeset.changes changeset.changes
end end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
ap_types = ap_types = convert_and_filter_mastodon_types(mastodon_types)
mastodon_types
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
query where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
ap_types = convert_and_filter_mastodon_types(mastodon_types)
where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
@ -95,4 +98,10 @@ defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
end end
defp restrict(query, _, _), do: query defp restrict(query, _, _), do: query
defp convert_and_filter_mastodon_types(types) do
types
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
end
end end

View file

@ -5,13 +5,28 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.HTML alias Pleroma.FollowingRelationship
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users} = opts) do def render("index.json", %{users: users} = opts) do
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
UserRelationship.view_relationships_option(opts[:for], users)
end
opts = Map.put(opts, :relationships, relationships_opt)
users users
|> render_many(AccountView, "show.json", opts) |> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1) |> Enum.filter(&Enum.any?/1)
@ -36,37 +51,111 @@ def render("relationship.json", %{user: nil, target: _target}) do
%{} %{}
end end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do def render(
follow_state = User.get_cached_follow_state(user, target) "relationship.json",
%{user: %User{} = reading_user, target: %User{} = target} = opts
) do
user_relationships = get_in(opts, [:relationships, :user_relationships])
following_relationships = get_in(opts, [:relationships, :following_relationships])
requested = follow_state =
if follow_state && !User.following?(user, target) do if following_relationships do
follow_state == "pending" user_to_target_following_relation =
FollowingRelationship.find(following_relationships, reading_user, target)
User.get_follow_state(reading_user, target, user_to_target_following_relation)
else else
false User.get_follow_state(reading_user, target)
end end
followed_by =
if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: "accept"} -> true
_ -> false
end
else
User.following?(target, reading_user)
end
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{ %{
id: to_string(target.id), id: to_string(target.id),
following: User.following?(user, target), following: follow_state == "accept",
followed_by: User.following?(target, user), followed_by: followed_by,
blocking: User.blocks_user?(user, target), blocking:
blocked_by: User.blocks_user?(target, user), UserRelationship.exists?(
muting: User.mutes?(user, target), user_relationships,
muting_notifications: User.muted_notifications?(user, target), :block,
subscribing: User.subscribed_to?(user, target), reading_user,
requested: requested, target,
domain_blocking: User.blocks_domain?(user, target), &User.blocks_user?(&1, &2)
showing_reblogs: User.showing_reblogs?(user, target), ),
blocked_by:
UserRelationship.exists?(
user_relationships,
:block,
target,
reading_user,
&User.blocks_user?(&1, &2)
),
muting:
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
),
muting_notifications:
UserRelationship.exists?(
user_relationships,
:notification_mute,
reading_user,
target,
&User.muted_notifications?(&1, &2)
),
subscribing:
UserRelationship.exists?(
user_relationships,
:inverse_subscription,
target,
reading_user,
&User.subscribed_to?(&2, &1)
),
requested: follow_state == "pending",
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
not UserRelationship.exists?(
user_relationships,
:reblog_mute,
reading_user,
target,
&User.muting_reblogs?(&1, &2)
),
endorsed: false endorsed: false
} }
end end
def render("relationships.json", %{user: user, targets: targets}) do def render("relationships.json", %{user: user, targets: targets} = opts) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target) relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
UserRelationship.view_relationships_option(user, targets)
end
render_opts = %{as: :target, user: user, relationships: relationships_opt}
render_many(targets, AccountView, "relationship.json", render_opts)
end end
defp do_render("show.json", %{user: user} = opts) do defp do_render("show.json", %{user: user} = opts) do
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname display_name = user.name || user.nickname
image = User.avatar_url(user) |> MediaProxy.url() image = User.avatar_url(user) |> MediaProxy.url()
@ -100,18 +189,12 @@ defp do_render("show.json", %{user: user} = opts) do
} }
end) end)
fields = relationship =
user render("relationship.json", %{
|> User.fields() user: opts[:for],
|> Enum.map(fn %{"name" => name, "value" => value} -> target: user,
%{ relationships: opts[:relationships]
"name" => name, })
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
%{ %{
id: to_string(user.id), id: to_string(user.id),
@ -123,17 +206,17 @@ defp do_render("show.json", %{user: user} = opts) do
followers_count: followers_count, followers_count: followers_count,
following_count: following_count, following_count: following_count,
statuses_count: user.note_count, statuses_count: user.note_count,
note: bio || "", note: user.bio || "",
url: User.profile_url(user), url: User.profile_url(user),
avatar: image, avatar: image,
avatar_static: image, avatar_static: image,
header: header, header: header,
header_static: header, header_static: header,
emojis: emojis, emojis: emojis,
fields: fields, fields: user.fields,
bot: bot, bot: bot,
source: %{ source: %{
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")), note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
sensitive: false, sensitive: false,
fields: user.raw_fields, fields: user.raw_fields,
pleroma: %{ pleroma: %{

View file

@ -8,24 +8,86 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
safe_render_many(notifications, NotificationView, "show.json", %{for: user}) activities = Enum.map(notifications, & &1.activity)
parent_activities =
activities
|> Enum.filter(
&(Activity.mastodon_notification_type(&1) in [
"favourite",
"reblog",
"pleroma:emoji_reaction"
])
)
|> Enum.map(& &1.data["object"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Pleroma.Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
move_activities_targets =
activities
|> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
actors =
activities
|> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end)
|> Enum.filter(& &1)
|> Kernel.++(move_activities_targets)
UserRelationship.view_relationships_option(reading_user, actors)
end
opts = %{
for: reading_user,
parent_activities: parent_activities,
relationships: relationships_opt
}
safe_render_many(notifications, NotificationView, "show.json", opts)
end end
def render("show.json", %{ def render(
notification: %Notification{activity: activity} = notification, "show.json",
for: user %{
}) do notification: %Notification{activity: activity} = notification,
for: reading_user
} = opts
) do
actor = User.get_cached_by_ap_id(activity.data["actor"]) actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
parent_activity_fn = fn ->
if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
else
Activity.get_create_by_object_ap_id(activity.data["object"])
end
end
mastodon_type = Activity.mastodon_notification_type(activity) mastodon_type = Activity.mastodon_notification_type(activity)
with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do with %{id: _} = account <-
AccountView.render("show.json", %{
user: actor,
for: reading_user,
relationships: opts[:relationships]
}) do
response = %{ response = %{
id: to_string(notification.id), id: to_string(notification.id),
type: mastodon_type, type: mastodon_type,
@ -36,24 +98,28 @@ def render("show.json", %{
} }
} }
render_opts = %{relationships: opts[:relationships]}
case mastodon_type do case mastodon_type do
"mention" -> "mention" ->
put_status(response, activity, user) put_status(response, activity, reading_user, render_opts)
"favourite" -> "favourite" ->
put_status(response, parent_activity, user) put_status(response, parent_activity_fn.(), reading_user, render_opts)
"reblog" -> "reblog" ->
put_status(response, parent_activity, user) put_status(response, parent_activity_fn.(), reading_user, render_opts)
"move" -> "move" ->
put_target(response, activity, user) put_target(response, activity, reading_user, render_opts)
"follow" -> "follow" ->
response response
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
put_status(response, parent_activity, user) |> put_emoji(activity) response
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|> put_emoji(activity)
_ -> _ ->
nil nil
@ -64,16 +130,21 @@ def render("show.json", %{
end end
defp put_emoji(response, activity) do defp put_emoji(response, activity) do
response Map.put(response, :emoji, activity.data["content"])
|> Map.put(:emoji, activity.data["content"])
end end
defp put_status(response, activity, user) do defp put_status(response, activity, reading_user, opts) do
Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
status_render = StatusView.render("show.json", status_render_opts)
Map.put(response, :status, status_render)
end end
defp put_target(response, activity, user) do defp put_target(response, activity, reading_user, opts) do
target = User.get_cached_by_ap_id(activity.data["target"]) target_user = User.get_cached_by_ap_id(activity.data["target"])
Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) target_render_opts = Map.merge(opts, %{user: target_user, for: reading_user})
target_render = AccountView.render("show.json", target_render_opts)
Map.put(response, :target, target_render)
end end
end end

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
@ -71,10 +72,41 @@ defp reblogged?(activity, user) do
end end
def render("index.json", opts) do def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities) # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
opts = Map.put(opts, :replied_to_activities, replied_to_activities) activities = Enum.filter(opts.activities, & &1)
replied_to_activities = get_replied_to_activities(activities)
safe_render_many(opts.activities, StatusView, "show.json", opts) parent_activities =
activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
|> Enum.map(&Object.normalize(&1).data["id"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
UserRelationship.view_relationships_option(opts[:for], actors)
end
opts =
opts
|> Map.put(:replied_to_activities, replied_to_activities)
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
safe_render_many(activities, StatusView, "show.json", opts)
end end
def render( def render(
@ -85,17 +117,25 @@ def render(
created_at = Utils.to_masto_date(activity.data["published"]) created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity) activity_object = Object.normalize(activity)
reblogged_activity = reblogged_parent_activity =
Activity.create_by_object_ap_id(activity_object.data["id"]) if opts[:parent_activities] do
|> Activity.with_preloaded_bookmark(opts[:for]) Activity.Queries.find_by_object_ap_id(
|> Activity.with_set_thread_muted_field(opts[:for]) opts[:parent_activities],
|> Repo.one() activity_object.data["id"]
)
else
Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
end
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity)) reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
mentions = mentions =
activity.recipients activity.recipients
@ -107,7 +147,12 @@ def render(
id: to_string(activity.id), id: to_string(activity.id),
uri: activity_object.data["id"], uri: activity_object.data["id"],
url: activity_object.data["id"], url: activity_object.data["id"],
account: AccountView.render("show.json", %{user: user, for: opts[:for]}), account:
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
reblog: reblogged, reblog: reblogged,
@ -116,7 +161,7 @@ def render(
reblogs_count: 0, reblogs_count: 0,
replies_count: 0, replies_count: 0,
favourites_count: 0, favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]), reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited), favourited: present?(favorited),
bookmarked: present?(bookmarked), bookmarked: present?(bookmarked),
muted: false, muted: false,
@ -183,9 +228,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
end end
thread_muted? = thread_muted? =
case activity.thread_muted? do cond do
thread_muted? when is_boolean(thread_muted?) -> thread_muted? is_nil(opts[:for]) -> false
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false is_boolean(activity.thread_muted?) -> activity.thread_muted?
true -> CommonAPI.thread_muted?(opts[:for], activity)
end end
attachment_data = object.data["attachment"] || [] attachment_data = object.data["attachment"] || []
@ -253,11 +299,26 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
_ -> [] _ -> []
end end
muted =
thread_muted? ||
UserRelationship.exists?(
get_in(opts, [:relationships, :user_relationships]),
:mute,
opts[:for],
user,
fn for_user, user -> User.mutes?(for_user, user) end
)
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object.data["id"], uri: object.data["id"],
url: url, url: url,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}), account:
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
}),
in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
@ -270,7 +331,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
reblogged: reblogged?(activity, opts[:for]), reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited), favourited: present?(favorited),
bookmarked: present?(bookmarked), bookmarked: present?(bookmarked),
muted: thread_muted? || User.mutes?(opts[:for], user), muted: muted,
pinned: pinned?(activity, user), pinned: pinned?(activity, user),
sensitive: sensitive, sensitive: sensitive,
spoiler_text: summary, spoiler_text: summary,

View file

@ -60,6 +60,7 @@ def raw_nodeinfo do
"pleroma_explicit_addressing", "pleroma_explicit_addressing",
"shareable_emoji_packs", "shareable_emoji_packs",
"multifetch", "multifetch",
"pleroma:api/v1/notifications:include_types_filter",
if Config.get([:media_proxy, :enabled]) do if Config.get([:media_proxy, :enabled]) do
"media_proxy" "media_proxy"
end, end,

View file

@ -16,6 +16,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Metadata.PlayerView
alias Pleroma.Web.Router alias Pleroma.Web.Router
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
)
plug( plug(
RateLimiter, RateLimiter,
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
@ -135,13 +139,13 @@ def notice_player(conn, %{"id" => id}) do
end end
end end
def errors(conn, {:error, :not_found}) do defp errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found") render_error(conn, :not_found, "Not found")
end end
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) defp errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do defp errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong") render_error(conn, :internal_server_error, "Something went wrong")
end end
end end

View file

@ -101,6 +101,11 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id})
conn conn
|> put_view(ConversationView) |> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user}) |> render("participation.json", %{participation: participation, for: user})
else
_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end end
end end
@ -108,9 +113,9 @@ def conversation_statuses(
%{assigns: %{user: user}} = conn, %{assigns: %{user: user}} = conn,
%{"id" => participation_id} = params %{"id" => participation_id} = params
) do ) do
participation = Participation.get(participation_id, preload: [:conversation]) with %Participation{} = participation <-
Participation.get(participation_id, preload: [:conversation]),
if user.id == participation.user_id do true <- user.id == participation.user_id do
params = params =
params params
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
@ -126,6 +131,11 @@ def conversation_statuses(
|> add_link_headers(activities) |> add_link_headers(activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
else
_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end end
end end
@ -133,15 +143,22 @@ def update_conversation(
%{assigns: %{user: user}} = conn, %{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients} %{"id" => participation_id, "recipients" => recipients}
) do ) do
participation = with %Participation{} = participation <- Participation.get(participation_id),
participation_id true <- user.id == participation.user_id,
|> Participation.get()
with true <- user.id == participation.user_id,
{:ok, participation} <- Participation.set_recipients(participation, recipients) do {:ok, participation} <- Participation.set_recipients(participation, recipients) do
conn conn
|> put_view(ConversationView) |> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user}) |> render("participation.json", %{participation: participation, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end end
end end

View file

@ -173,6 +173,8 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)
@ -513,7 +515,7 @@ defmodule Pleroma.Web.Router do
end end
pipeline :ostatus do pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"]) plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Plugs.StaticFEPlug) plug(Pleroma.Plugs.StaticFEPlug)
end end
@ -541,6 +543,7 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end end
# Server to Server (S2S) AP interactions
pipeline :activitypub do pipeline :activitypub do
plug(:accepts, ["activity+json", "json"]) plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug) plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
@ -554,6 +557,7 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/users/:nickname/outbox", ActivityPubController, :outbox)
end end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do pipeline :activitypub_client do
plug(:accepts, ["activity+json", "json"]) plug(:accepts, ["activity+json", "json"])
plug(:fetch_session) plug(:fetch_session)
@ -597,8 +601,8 @@ defmodule Pleroma.Web.Router do
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
end end
get("/following", ActivityPubController, :following, assigns: %{relay: true}) get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :followers, assigns: %{relay: true}) get("/followers", ActivityPubController, :relay_followers)
end end
scope "/internal/fetch", Pleroma.Web.ActivityPub do scope "/internal/fetch", Pleroma.Web.ActivityPub do

View file

@ -17,6 +17,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
plug(:assign_id) plug(:assign_id)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"] @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
@ -33,7 +37,7 @@ defp not_found(conn, message) do
|> render("error.html", %{message: message, meta: ""}) |> render("error.html", %{message: message, meta: ""})
end end
def get_counts(%Activity{} = activity) do defp get_counts(%Activity{} = activity) do
%Object{data: data} = Object.normalize(activity) %Object{data: data} = Object.normalize(activity)
%{ %{
@ -43,9 +47,9 @@ def get_counts(%Activity{} = activity) do
} }
end end
def represent(%Activity{} = activity), do: represent(activity, false) defp represent(%Activity{} = activity), do: represent(activity, false)
def represent(%Activity{object: %Object{data: data}} = activity, selected) do defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
{:ok, user} = User.get_or_fetch(activity.object.data["actor"]) {:ok, user} = User.get_or_fetch(activity.object.data["actor"])
link = link =
@ -54,10 +58,19 @@ def represent(%Activity{object: %Object{data: data}} = activity, selected) do
_ -> data["url"] || data["external_url"] || data["id"] _ -> data["url"] || data["external_url"] || data["id"]
end end
content =
if data["content"] do
data["content"]
|> Pleroma.HTML.filter_tags()
|> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
else
nil
end
%{ %{
user: user, user: User.sanitize_html(user),
title: get_title(activity.object), title: get_title(activity.object),
content: data["content"] || nil, content: content,
attachment: data["attachment"], attachment: data["attachment"],
link: link, link: link,
published: data["published"], published: data["published"],
@ -109,7 +122,7 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
next_page_id = List.last(timeline) && List.last(timeline).id next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{ render(conn, "profile.html", %{
user: user, user: User.sanitize_html(user),
timeline: timeline, timeline: timeline,
prev_page_id: prev_page_id, prev_page_id: prev_page_id,
next_page_id: next_page_id, next_page_id: next_page_id,
@ -147,17 +160,17 @@ def show(%{assigns: %{activity_id: _}} = conn, _params) do
end end
end end
def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id) do: assign(conn, :notice_id, notice_id)
def assign_id(%{path_info: ["users", user_id]} = conn, _opts), defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id) do: assign(conn, :username_or_id, user_id)
def assign_id(%{path_info: ["objects", object_id]} = conn, _opts), defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
do: assign(conn, :object_id, object_id) do: assign(conn, :object_id, object_id)
def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts), defp assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
do: assign(conn, :activity_id, activity_id) do: assign(conn, :activity_id, activity_id)
def assign_id(conn, _opts), do: conn defp assign_id(conn, _opts), do: conn
end end

View file

@ -130,7 +130,7 @@ defp do_stream(%{topic: topic, item: item}) do
defp should_send?(%User{} = user, %Activity{} = item) do defp should_send?(%User{} = user, %Activity{} = item) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
recipients = MapSet.new(item.recipients) recipients = MapSet.new(item.recipients)

View file

@ -0,0 +1,49 @@
<item>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<guid><%= @data["id"] %></guid>
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_content(@object) %></description>
<pubDate><%= @data["published"] %></pubDate>
<updated><%= @data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<link rel="ostatus:conversation"><%= activity_context(@activity) %></link>
<%= if @data["summary"] do %>
<description><%= @data["summary"] %></description>
<% end %>
<%= if @activity.local do %>
<link><%= @data["id"] %></link>
<% else %>
<link><%= @data["external_url"] %></link>
<% end %>
<%= for tag <- @data["tag"] || [] do %>
<category term="<%= tag %>"></category>
<% end %>
<%= for attachment <- @data["attachment"] || [] do %>
<link type="<%= attachment_type(attachment) %>"><%= attachment_href(attachment) %></link>
<% end %>
<%= if @data["inReplyTo"] do %>
<thr:in-reply-to ref='<%= @data["inReplyTo"] %>' href='<%= get_href(@data["inReplyTo"]) %>'/>
<% end %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection">http://activityschema.org/collection/public</link>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) do %>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person"><%= id %></link>
<% end %>
<% end %>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji"><%= file %></link>
<% end %>
</item>

View file

@ -0,0 +1,17 @@
<managingEditor>
<guid><%= @user.ap_id %></guid>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<uri><%= @user.ap_id %></uri>
<poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>
<poco:displayName><%= @user.name %></poco:displayName>
<poco:note><%= escape(@user.bio) %></poco:note>
<description><%= escape(@user.bio) %></description>
<name><%= @user.nickname %></name>
<link rel="avatar"><%= User.avatar_url(@user) %></link>
<%= if User.banner_url(@user) do %>
<link rel="header"><%= User.banner_url(@user) %></link>
<% end %>
<%= if @user.local do %>
<ap_enabled>true</ap_enabled>
<% end %>
</managingEditor>

View file

@ -12,13 +12,13 @@
<logo><%= logo(@user) %></logo> <logo><%= logo(@user) %></logo>
<link rel="self" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/> <link rel="self" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
<%= render @view_module, "_author.xml", assigns %> <%= render @view_module, "_author.atom", assigns %>
<%= if last_activity(@activities) do %> <%= if last_activity(@activities) do %>
<link rel="next" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/> <link rel="next" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
<% end %> <% end %>
<%= for activity <- @activities do %> <%= for activity <- @activities do %>
<%= render @view_module, "_activity.xml", Map.merge(assigns, prepare_activity(activity)) %> <%= render @view_module, "_activity.atom", Map.merge(assigns, prepare_activity(activity)) %>
<% end %> <% end %>
</feed> </feed>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<guid><%= user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %></guid>
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<image><%= logo(@user) %></image>
<link><%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
<%= render @view_module, "_author.rss", assigns %>
<%= if last_activity(@activities) do %>
<link rel="next"><%= '#{user_feed_url(@conn, :feed, @user.nickname)}.rss?max_id=#{last_activity(@activities).id}' %></link>
<% end %>
<%= for activity <- @activities do %>
<%= render @view_module, "_activity.rss", Map.merge(assigns, prepare_activity(activity)) %>
<% end %>
</channel>
</rss>

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"] @status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
plug(Pleroma.Web.FederatingPlug)
# Note: follower can submit the form (with password auth) not being signed in (having no token) # Note: follower can submit the form (with password auth) not being signed in (having no token)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,

View file

@ -17,6 +17,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} %{scopes: ["follow", "write:follows"]}

View file

@ -10,10 +10,6 @@ defmodule Pleroma.Workers.BackgroundWorker do
use Pleroma.Workers.WorkerHelper, queue: "background" use Pleroma.Workers.WorkerHelper, queue: "background"
@impl Oban.Worker @impl Oban.Worker
def perform(%{"op" => "fetch_initial_posts", "user_id" => user_id}, _job) do
user = User.get_cached_by_id(user_id)
User.perform(:fetch_initial_posts, user)
end
def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do def project do
[ [
app: :pleroma, app: :pleroma,
version: version("1.1.50"), version: version("2.0.50"),
elixir: "~> 1.8", elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(), compilers: [:phoenix, :gettext] ++ Mix.compilers(),
@ -63,7 +63,7 @@ def copy_nginx_config(%{path: target_path} = release) do
def application do def application do
[ [
mod: {Pleroma.Application, []}, mod: {Pleroma.Application, []},
extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize], extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :ssl],
included_applications: [:ex_syslogger] included_applications: [:ex_syslogger]
] ]
end end
@ -126,7 +126,7 @@ defp deps do
{:ex_aws_s3, "~> 2.0"}, {:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.6.6"}, {:sweet_xml, "~> 0.6.6"},
{:earmark, "~> 1.3"}, {:earmark, "~> 1.3"},
{:bbcode, "~> 0.1.1"}, {:bbcode_pleroma, "~> 0.2.0"},
{:ex_machina, "~> 2.3", only: :test}, {:ex_machina, "~> 2.3", only: :test},
{:credo, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.3", only: :test}, {:mock, "~> 0.3.3", only: :test},

View file

@ -4,6 +4,7 @@
"base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "3b29948de2013d3f93aa898c884a9dff847e7aec75d9d6d8c1dc4c61c2716c42"}, "base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "3b29948de2013d3f93aa898c884a9dff847e7aec75d9d6d8c1dc4c61c2716c42"},
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"},
"bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"}, "bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"}, "cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"},

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.FixModerationLogSubjects do
use Ecto.Migration
def change do
execute(
"update moderation_log set data = safe_jsonb_set(data, '{subject}', safe_jsonb_set('[]'::jsonb, '{0}', data->'subject')) where jsonb_typeof(data->'subject') != 'array' and data->>'action' = ANY('{revoke,grant,activate,deactivate,delete}');"
)
end
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Repo.Migrations.ConfigRemoveFetchInitialPosts do
use Ecto.Migration
def change do
execute(
"delete from config where config.key = ':fetch_initial_posts' and config.group = ':pleroma';",
""
)
end
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Repo.Migrations.DeleteFetchInitialPostsJobs do
use Ecto.Migration
def change do
execute(
"delete from oban_jobs where worker = 'Pleroma.Workers.BackgroundWorker' and args->>'op' = 'fetch_initial_posts';",
""
)
end
end

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