forked from AkkomaGang/akkoma
Merge branch 'develop' into issue/1276
This commit is contained in:
commit
dfd2c74184
259 changed files with 4031 additions and 2007 deletions
|
@ -62,19 +62,21 @@ unit-testing:
|
|||
- mix ecto.migrate
|
||||
- mix coveralls --preload-modules
|
||||
|
||||
federated-testing:
|
||||
stage: test
|
||||
cache: *testing_cache_policy
|
||||
services:
|
||||
- name: minibikini/postgres-with-rum:12
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- epmd -daemon
|
||||
- mix test --trace --only federated
|
||||
# Removed to fix CI issue. In this early state it wasn't adding much value anyway.
|
||||
# TODO Fix and reinstate federated testing
|
||||
# federated-testing:
|
||||
# stage: test
|
||||
# cache: *testing_cache_policy
|
||||
# services:
|
||||
# - name: minibikini/postgres-with-rum:12
|
||||
# alias: postgres
|
||||
# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
# script:
|
||||
# - mix deps.get
|
||||
# - mix ecto.create
|
||||
# - mix ecto.migrate
|
||||
# - epmd -daemon
|
||||
# - mix test --trace --only federated
|
||||
|
||||
unit-testing-rum:
|
||||
stage: test
|
||||
|
|
20
.gitlab/issue_templates/Bug.md
Normal file
20
.gitlab/issue_templates/Bug.md
Normal 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
|
5
.gitlab/merge_request_templates/Release.md
Normal file
5
.gitlab/merge_request_templates/Release.md
Normal 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
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -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/).
|
||||
|
||||
## [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
|
||||
- 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)
|
||||
- Logger: default log level changed from `warn` to `info`.
|
||||
- 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.
|
||||
- Instance stats are now loaded on startup instead of being empty until next hourly job.
|
||||
<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: 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.
|
||||
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
|
||||
</details>
|
||||
|
||||
### 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
|
||||
</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
|
||||
### Fixed
|
||||
- Not being able to log into to third party apps when the browser is logged into mastofe
|
||||
|
|
|
@ -22,9 +22,10 @@ def generate_like_activities(user, posts) do
|
|||
|
||||
def generate_users(opts) do
|
||||
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
|
||||
|
||||
defp do_generate_users(opts) do
|
||||
|
|
76
benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex
Normal file
76
benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex
Normal 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
|
|
@ -61,8 +61,6 @@
|
|||
|
||||
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
|
||||
|
||||
config :pleroma_job_queue, disabled: true
|
||||
|
||||
config :pleroma, Pleroma.ScheduledActivity,
|
||||
daily_user_limit: 2,
|
||||
total_user_limit: 3,
|
||||
|
|
|
@ -504,10 +504,6 @@
|
|||
federator_outgoing: 5
|
||||
]
|
||||
|
||||
config :pleroma, :fetch_initial_posts,
|
||||
enabled: false,
|
||||
pages: 5
|
||||
|
||||
config :auto_linker,
|
||||
opts: [
|
||||
extra: true,
|
||||
|
@ -628,6 +624,11 @@
|
|||
parameters: [gin_fuzzy_search_limit: "500"],
|
||||
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
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
|
@ -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,
|
||||
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,
|
||||
key: :opts,
|
||||
|
@ -2480,7 +2442,7 @@
|
|||
%{
|
||||
key: :relations_actions,
|
||||
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}]]
|
||||
},
|
||||
%{
|
||||
|
@ -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,
|
||||
key: :chat,
|
||||
|
@ -2966,5 +2915,65 @@
|
|||
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."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -92,6 +92,8 @@
|
|||
|
||||
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
|
||||
|
||||
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
||||
|
||||
if File.exists?("./config/test.secret.exs") do
|
||||
import_config "test.secret.exs"
|
||||
else
|
||||
|
|
|
@ -414,6 +414,83 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
- `nicknames`
|
||||
- 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 a list of reports
|
||||
|
|
|
@ -117,7 +117,7 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
|
|||
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`.
|
||||
- `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`
|
||||
|
||||
|
@ -180,7 +180,7 @@ Post here request with grant_type=refresh_token to obtain new access token. Retu
|
|||
## Account Registration
|
||||
`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
|
||||
* `bio`: optional
|
||||
* `captcha_solution`: optional, contains provider-specific captcha solution,
|
||||
|
|
|
@ -288,10 +288,11 @@ Pleroma Conversations have the same general structure that Mastodon Conversation
|
|||
2. Pleroma Conversations statuses can be requested by Conversation id.
|
||||
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.
|
||||
|
||||
⚠ 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`
|
||||
### Timeline for a given conversation
|
||||
|
|
|
@ -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.
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database remove_embedded_objects [<options>]
|
||||
./bin/pleroma_ctl database remove_embedded_objects [option ...]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database remove_embedded_objects [<options>]
|
||||
mix pleroma.database remove_embedded_objects [option ...]
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database prune_objects [<options>]
|
||||
./bin/pleroma_ctl database prune_objects [option ...]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database prune_objects [<options>]
|
||||
mix pleroma.database prune_objects [option ...]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
## Send digest email since given date (user registration date by default) ignoring user activity status.
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl digest test <nickname> [<since_date>]
|
||||
./bin/pleroma_ctl digest test <nickname> [since_date]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.digest test <nickname> [<since_date>]
|
||||
mix pleroma.digest test <nickname> [since_date]
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
## Lists emoji packs and metadata specified in the manifest
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji ls-packs [<options>]
|
||||
./bin/pleroma_ctl emoji ls-packs [option ...]
|
||||
```
|
||||
|
||||
```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`
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji get-packs [<options>] <packs>
|
||||
./bin/pleroma_ctl emoji get-packs [option ...] <pack ...>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.emoji get-packs [<options>] <packs>
|
||||
mix pleroma.emoji get-packs [option ...] <pack ...>
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
## Generate a new configuration file
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl instance gen [<options>]
|
||||
./bin/pleroma_ctl instance gen [option ...]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.instance gen [<options>]
|
||||
mix pleroma.instance gen [option ...]
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
## Migrate uploads from local to remote storage
|
||||
```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"
|
||||
mix pleroma.uploads migrate_local <target_uploader> [<options>]
|
||||
mix pleroma.uploads migrate_local <target_uploader> [option ...]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
## Create a user
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user new <email> [<options>]
|
||||
./bin/pleroma_ctl user new <nickname> <email> [option ...]
|
||||
```
|
||||
|
||||
```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
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user invite [<options>]
|
||||
./bin/pleroma_ctl user invite [option ...]
|
||||
```
|
||||
|
||||
```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
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user set <nickname> [<options>]
|
||||
./bin/pleroma_ctl user set <nickname> [option ...]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user set <nickname> [<options>]
|
||||
mix pleroma.user set <nickname> [option ...]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
@ -18,9 +18,8 @@
|
|||
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.
|
||||
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:
|
||||
$ sudo -u postgres psql pleroma_database_name
|
||||
pleroma=# VACUUM ANALYZE;
|
||||
9. Run `sudo -Hu postgres vacuumdb --all --analyze-in-stages`. This will quickly generate the statistics so that postgres can properly plan queries.
|
||||
|
||||
[^1]: Prefix with `MIX_ENV=prod` to run it using the production config file.
|
||||
|
||||
## Remove
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# 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!
|
||||
|
||||
## Desktop
|
||||
|
|
|
@ -138,7 +138,8 @@ config :pleroma, :mrf_user_allowlist,
|
|||
```
|
||||
|
||||
#### :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:
|
||||
* `: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
|
||||
|
@ -151,14 +152,6 @@ config :pleroma, :mrf_user_allowlist,
|
|||
* `sign_object_fetches`: Sign object fetches with HTTP signatures
|
||||
* `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
|
||||
|
||||
* `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
|
||||
|
||||
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`
|
|
@ -156,8 +156,8 @@ cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf
|
|||
```
|
||||
|
||||
```sh tab="Debian/Ubuntu"
|
||||
cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx
|
||||
ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
|
||||
cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.conf
|
||||
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
|
||||
|
|
|
@ -90,8 +90,6 @@ server {
|
|||
proxy_ignore_client_abort on;
|
||||
proxy_buffering on;
|
||||
chunked_transfer_encoding on;
|
||||
proxy_ignore_headers Cache-Control;
|
||||
proxy_hide_header Cache-Control;
|
||||
proxy_pass http://127.0.0.1:4000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ def run(_) do
|
|||
defp do_run(implementation) do
|
||||
start_pleroma()
|
||||
|
||||
with descriptions <- Pleroma.Config.Loader.load("config/description.exs"),
|
||||
with descriptions <- Pleroma.Config.Loader.read("config/description.exs"),
|
||||
{:ok, file_path} <-
|
||||
Pleroma.Docs.Generator.process(
|
||||
implementation,
|
||||
|
|
|
@ -35,7 +35,7 @@ def run(["unfollow", target]) do
|
|||
def run(["list"]) do
|
||||
start_pleroma()
|
||||
|
||||
with {:ok, list} <- Relay.list() do
|
||||
with {:ok, list} <- Relay.list(true) do
|
||||
list |> Enum.each(&shell_info(&1))
|
||||
else
|
||||
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
|
||||
|
|
|
@ -95,6 +95,17 @@ def with_preloaded_object(query, join_type \\ :inner) do
|
|||
|> preload([activity, object: object], object: object)
|
||||
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
|
||||
join(query, join_type, [activity], u in User,
|
||||
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))
|
||||
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
|
||||
deactivated_users =
|
||||
from(u in User.Query.build(%{deactivated: true}), select: u.ap_id)
|
||||
|
|
|
@ -39,7 +39,7 @@ defp visibility_tags(object, activity) do
|
|||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -35,6 +35,13 @@ def by_author(query \\ Activity, %User{ap_id: ap_id}) do
|
|||
from(a in query, where: a.actor == ^ap_id)
|
||||
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
|
||||
def by_object_id(query \\ Activity, object_id)
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ def user_agent do
|
|||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
Pleroma.HTML.compile_scrubbers()
|
||||
Pleroma.Config.DeprecationWarnings.warn()
|
||||
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
|
||||
|
|
|
@ -3,14 +3,33 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.Holder do
|
||||
@config Pleroma.Config.Loader.load_and_merge()
|
||||
@config Pleroma.Config.Loader.default_config()
|
||||
|
||||
@spec config() :: keyword()
|
||||
def config, do: @config
|
||||
@spec save_default() :: :ok
|
||||
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()
|
||||
def config(group), do: @config[group]
|
||||
Pleroma.Config.Loader.merge(@config, release_config)
|
||||
else
|
||||
@config
|
||||
end
|
||||
|
||||
@spec config(atom(), atom()) :: any()
|
||||
def config(group, key), do: @config[group][key]
|
||||
Pleroma.Config.put(:default_config, default_config)
|
||||
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
|
||||
|
|
|
@ -13,32 +13,28 @@ defmodule Pleroma.Config.Loader do
|
|||
]
|
||||
|
||||
if Code.ensure_loaded?(Config.Reader) do
|
||||
@spec load(Path.t()) :: keyword()
|
||||
def load(path), do: Config.Reader.read!(path)
|
||||
@reader Config.Reader
|
||||
|
||||
defp do_merge(conf1, conf2), do: Config.Reader.merge(conf1, conf2)
|
||||
def read(path), do: @reader.read!(path)
|
||||
else
|
||||
# support for Elixir less than 1.9
|
||||
@spec load(Path.t()) :: keyword()
|
||||
def load(path) do
|
||||
@reader Mix.Config
|
||||
def read(path) do
|
||||
path
|
||||
|> Mix.Config.eval!()
|
||||
|> @reader.eval!()
|
||||
|> elem(0)
|
||||
end
|
||||
|
||||
defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2)
|
||||
end
|
||||
|
||||
@spec load_and_merge() :: keyword()
|
||||
def load_and_merge do
|
||||
all_paths =
|
||||
if Pleroma.Config.get(:release),
|
||||
do: ["config/config.exs", "config/releases.exs"],
|
||||
else: ["config/config.exs"]
|
||||
@spec read(Path.t()) :: keyword()
|
||||
|
||||
all_paths
|
||||
|> Enum.map(&load(&1))
|
||||
|> Enum.reduce([], &do_merge(&2, &1))
|
||||
@spec merge(keyword(), keyword()) :: keyword()
|
||||
def merge(c1, c2), do: @reader.merge(c1, c2)
|
||||
|
||||
@spec default_config() :: keyword()
|
||||
def default_config do
|
||||
"config/config.exs"
|
||||
|> read()
|
||||
|> filter()
|
||||
end
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ defp merge_and_update(setting) do
|
|||
key = ConfigDB.from_string(setting.key)
|
||||
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)
|
||||
|
||||
merged_value =
|
||||
|
|
|
@ -129,21 +129,18 @@ def for_user(user, params \\ %{}) do
|
|||
end
|
||||
|
||||
def restrict_recipients(query, user, %{"recipients" => user_ids}) do
|
||||
user_ids =
|
||||
user_binary_ids =
|
||||
[user.id | user_ids]
|
||||
|> Enum.uniq()
|
||||
|> Enum.reduce([], fn user_id, acc ->
|
||||
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
|
||||
[user_id | acc]
|
||||
end)
|
||||
|> User.binary_id()
|
||||
|
||||
conversation_subquery =
|
||||
__MODULE__
|
||||
|> group_by([p], p.conversation_id)
|
||||
|> having(
|
||||
[p],
|
||||
count(p.user_id) == ^length(user_ids) and
|
||||
fragment("array_agg(?) @> ?", p.user_id, ^user_ids)
|
||||
count(p.user_id) == ^length(user_binary_ids) and
|
||||
fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids)
|
||||
)
|
||||
|> select([p], %{id: p.conversation_id})
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ def process(descriptions) do
|
|||
end
|
||||
|
||||
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]
|
||||
|> Pleroma.Docs.Generator.convert_to_strings()
|
||||
|> Jason.encode!()
|
||||
|
|
256
lib/pleroma/earmark_renderer.ex
Normal file
256
lib/pleroma/earmark_renderer.ex
Normal 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
|
|
@ -129,4 +129,32 @@ def move_following(origin, target) do
|
|||
move_following(origin, target)
|
||||
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
|
||||
|
|
|
@ -387,24 +387,6 @@ def get_log_entry_message(%ModerationLog{
|
|||
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
|
||||
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()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
|
@ -416,24 +398,6 @@ def get_log_entry_message(%ModerationLog{
|
|||
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
|
||||
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()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
|
@ -473,26 +437,6 @@ def get_log_entry_message(%ModerationLog{
|
|||
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
|
||||
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()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
|
@ -505,26 +449,6 @@ def get_log_entry_message(%ModerationLog{
|
|||
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
|
||||
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()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
|
@ -681,6 +605,17 @@ def get_log_entry_message(%ModerationLog{
|
|||
}"
|
||||
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
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Notification do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.Push
|
||||
|
@ -19,6 +20,7 @@ defmodule Pleroma.Notification do
|
|||
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
require Logger
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
@ -50,11 +52,11 @@ def last_read_query(user) do
|
|||
end
|
||||
|
||||
defp for_user_query_ap_id_opts(user, opts) do
|
||||
ap_id_relations =
|
||||
ap_id_relationships =
|
||||
[:block] ++
|
||||
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)
|
||||
|
||||
|
@ -90,7 +92,6 @@ def for_user_query(user, opts \\ %{}) do
|
|||
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|
||||
|> exclude_blocked(user, exclude_blocked_opts)
|
||||
|> exclude_visibility(opts)
|
||||
|> exclude_move(opts)
|
||||
end
|
||||
|
||||
defp exclude_blocked(query, user, opts) do
|
||||
|
@ -114,20 +115,12 @@ defp exclude_notification_muted(query, user, opts) do
|
|||
|
||||
query
|
||||
|> 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)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
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]
|
||||
|
||||
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
|
||||
object = Object.normalize(activity)
|
||||
|
||||
unless object && object.data["type"] == "Answer" do
|
||||
notifications =
|
||||
activity
|
||||
|> get_notified_from_activity()
|
||||
|> Enum.map(&create_notification(activity, &1))
|
||||
|
||||
{:ok, notifications}
|
||||
else
|
||||
if object && object.data["type"] == "Answer" do
|
||||
{:ok, []}
|
||||
else
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||
notifications =
|
||||
activity
|
||||
|> get_notified_from_activity()
|
||||
|> Enum.map(&create_notification(activity, &1))
|
||||
|
||||
{:ok, notifications}
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
|
||||
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.
|
||||
def create_notification(%Activity{} = activity, %User{} = user) do
|
||||
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
|
||||
unless skip?(activity, user) do
|
||||
{:ok, %{notification: notification}} =
|
||||
Multi.new()
|
||||
|
@ -335,31 +331,78 @@ def create_notification(%Activity{} = activity, %User{} = user) do
|
|||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
|
||||
["user", "user:notification"]
|
||||
|> Streamer.stream(notification)
|
||||
if do_send do
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end
|
||||
|
||||
Push.send(notification)
|
||||
notification
|
||||
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{data: %{"type" => type}} = activity, local_only)
|
||||
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
|> Utils.maybe_notify_followers(activity)
|
||||
|> Enum.uniq()
|
||||
|> User.get_users_from_set(local_only)
|
||||
potential_receiver_ap_ids =
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
|> Utils.maybe_notify_followers(activity)
|
||||
|> 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
|
||||
|
||||
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()
|
||||
def skip?(activity, user) do
|
||||
def skip?(%Activity{} = activity, %User{} = user) do
|
||||
[
|
||||
:self,
|
||||
:followers,
|
||||
|
@ -368,18 +411,20 @@ def skip?(activity, user) do
|
|||
:non_follows,
|
||||
:recently_followed
|
||||
]
|
||||
|> Enum.any?(&skip?(&1, activity, user))
|
||||
|> Enum.find(&skip?(&1, activity, user))
|
||||
end
|
||||
|
||||
def skip?(_, _), do: false
|
||||
|
||||
@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
|
||||
end
|
||||
|
||||
def skip?(
|
||||
:followers,
|
||||
activity,
|
||||
%{notification_settings: %{followers: false}} = user
|
||||
%Activity{} = activity,
|
||||
%User{notification_settings: %{followers: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
follower = User.get_cached_by_ap_id(actor)
|
||||
|
@ -388,15 +433,19 @@ def skip?(
|
|||
|
||||
def skip?(
|
||||
:non_followers,
|
||||
activity,
|
||||
%{notification_settings: %{non_followers: false}} = user
|
||||
%Activity{} = activity,
|
||||
%User{notification_settings: %{non_followers: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
follower = User.get_cached_by_ap_id(actor)
|
||||
!User.following?(follower, user)
|
||||
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"]
|
||||
followed = User.get_cached_by_ap_id(actor)
|
||||
User.following?(user, followed)
|
||||
|
@ -404,15 +453,16 @@ def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user
|
|||
|
||||
def skip?(
|
||||
:non_follows,
|
||||
activity,
|
||||
%{notification_settings: %{non_follows: false}} = user
|
||||
%Activity{} = activity,
|
||||
%User{notification_settings: %{non_follows: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
followed = User.get_cached_by_ap_id(actor)
|
||||
!User.following?(user, followed)
|
||||
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"]
|
||||
|
||||
Notification.for_user(user)
|
||||
|
|
|
@ -15,9 +15,24 @@ def call(%{assigns: %{user: %User{}}} = conn, _) do
|
|||
conn
|
||||
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
|
||||
|> render_error(:forbidden, "Invalid credentials.")
|
||||
|> halt
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,14 +10,20 @@ def init(options) do
|
|||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
if Pleroma.Config.get([:instance, :federating]) do
|
||||
if federating?() do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
|
||||
|> Phoenix.Controller.render("404.json")
|
||||
|> halt()
|
||||
fail(conn)
|
||||
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
|
||||
|
|
|
@ -78,7 +78,7 @@ def init(plug_opts) do
|
|||
end
|
||||
|
||||
def call(conn, plug_opts) do
|
||||
if disabled?() do
|
||||
if disabled?(conn) do
|
||||
handle_disabled(conn)
|
||||
else
|
||||
action_settings = action_settings(plug_opts)
|
||||
|
@ -87,9 +87,9 @@ def call(conn, plug_opts) do
|
|||
end
|
||||
|
||||
defp handle_disabled(conn) do
|
||||
if Config.get(:env) == :prod do
|
||||
Logger.warn("Rate limiter is disabled for localhost/socket")
|
||||
end
|
||||
Logger.warn(
|
||||
"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."
|
||||
)
|
||||
|
||||
conn
|
||||
end
|
||||
|
@ -109,16 +109,21 @@ defp handle(conn, action_settings) do
|
|||
end
|
||||
end
|
||||
|
||||
def disabled? do
|
||||
def disabled?(conn) do
|
||||
localhost_or_socket =
|
||||
Config.get([Pleroma.Web.Endpoint, :http, :ip])
|
||||
|> Tuple.to_list()
|
||||
|> Enum.join(".")
|
||||
|> String.match?(~r/^local|^127.0.0.1/)
|
||||
case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
|
||||
{127, 0, 0, 1} -> true
|
||||
{0, 0, 0, 0, 0, 0, 0, 1} -> true
|
||||
{: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
|
||||
|
||||
@inspect_bucket_not_found {:error, :not_found}
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
@headers ~w[
|
||||
|
@ -26,11 +28,12 @@ defmodule Pleroma.Plugs.RemoteIp do
|
|||
|
||||
def init(_), do: nil
|
||||
|
||||
def call(conn, _) do
|
||||
def call(%{remote_ip: original_remote_ip} = conn, _) do
|
||||
config = Pleroma.Config.get(__MODULE__, [])
|
||||
|
||||
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
|
||||
conn
|
||||
end
|
||||
|
|
|
@ -21,6 +21,9 @@ def call(conn, _) do
|
|||
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
|
||||
|
||||
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
|
||||
|
|
|
@ -14,9 +14,14 @@ defmodule Pleroma.Plugs.UploadedMedia do
|
|||
# no slashes
|
||||
@path "media"
|
||||
|
||||
@default_cache_control_header "public, max-age=1209600"
|
||||
|
||||
def init(_opts) do
|
||||
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(:at, "/__unconfigured_media_plug")
|
||||
|> Plug.Static.init()
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
|
||||
~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 ++
|
||||
~w(content-type content-disposition content-encoding content-range) ++
|
||||
~w(accept-ranges vary)
|
||||
|
@ -34,9 +34,6 @@ defmodule Pleroma.ReverseProxy do
|
|||
* request: `#{inspect(@keep_req_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:
|
||||
|
||||
* `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
|
||||
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
|
||||
has_cache_control? = List.keymember?(headers, "cache-control", 0)
|
||||
|
||||
cond do
|
||||
has_cache? && has_cache_control? ->
|
||||
headers
|
||||
|
||||
has_cache? ->
|
||||
# There's caching header present but no cache-control -- we need to explicitely override it
|
||||
# to public as Plug defaults to "max-age=0, private, must-revalidate"
|
||||
List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
|
||||
# There's caching header present but no cache-control -- we need to set our own
|
||||
# as Plug defaults to "max-age=0, private, must-revalidate"
|
||||
List.keystore(
|
||||
headers,
|
||||
"cache-control",
|
||||
0,
|
||||
{"cache-control", @default_cache_control_header}
|
||||
)
|
||||
|
||||
true ->
|
||||
List.keystore(
|
||||
|
|
|
@ -9,7 +9,8 @@ defmodule Pleroma.ThreadMute do
|
|||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
|
||||
require Ecto.Query
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
schema "thread_mutes" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
|
@ -18,19 +19,44 @@ defmodule Pleroma.ThreadMute do
|
|||
|
||||
def changeset(mute, params \\ %{}) do
|
||||
mute
|
||||
|> Ecto.Changeset.cast(params, [:user_id, :context])
|
||||
|> Ecto.Changeset.foreign_key_constraint(:user_id)
|
||||
|> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index)
|
||||
|> cast(params, [:user_id, :context])
|
||||
|> foreign_key_constraint(:user_id)
|
||||
|> unique_constraint(:user_id, name: :unique_index)
|
||||
end
|
||||
|
||||
def query(user_id, context) do
|
||||
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
|
||||
user_binary_id = User.binary_id(user_id)
|
||||
|
||||
ThreadMute
|
||||
|> Ecto.Query.where(user_id: ^user_id)
|
||||
|> Ecto.Query.where(context: ^context)
|
||||
|> where(user_id: ^user_binary_id)
|
||||
|> where(context: ^context)
|
||||
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
|
||||
%ThreadMute{}
|
||||
|> changeset(%{user_id: user_id, context: context})
|
||||
|
@ -42,8 +68,8 @@ def remove_mute(user_id, context) do
|
|||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def check_muted(user_id, context) do
|
||||
def exists?(user_id, context) do
|
||||
query(user_id, context)
|
||||
|> Repo.all()
|
||||
|> Repo.exists?()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Delivery
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
@ -149,22 +150,26 @@ defmodule Pleroma.User do
|
|||
{outgoing_relation, outgoing_relation_target},
|
||||
{incoming_relation, incoming_relation_source}
|
||||
]} <- @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,
|
||||
foreign_key: :source_id,
|
||||
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,
|
||||
foreign_key: :target_id,
|
||||
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])
|
||||
|
||||
# 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])
|
||||
end
|
||||
|
||||
|
@ -184,7 +189,9 @@ defmodule Pleroma.User do
|
|||
|
||||
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
|
||||
@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
|
||||
target_users_query = assoc(user, unquote(outgoing_relation_target))
|
||||
|
||||
|
@ -195,7 +202,8 @@ def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated?
|
|||
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
|
||||
__MODULE__
|
||||
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
|
||||
|
@ -205,7 +213,8 @@ def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
|
|||
|> Repo.all()
|
||||
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
|
||||
__MODULE__
|
||||
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
|
||||
|
@ -217,6 +226,24 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \
|
|||
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"
|
||||
@spec account_status(User.t()) :: account_status()
|
||||
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{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
|
||||
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{} = 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()
|
||||
def restrict_deactivated(query) do
|
||||
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_length(:bio, max: bio_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)
|
||||
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
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
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?)
|
||||
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
|
||||
struct
|
||||
|> cast(params, [:password, :password_confirmation])
|
||||
|
@ -472,10 +559,14 @@ def password_update_changeset(struct, params) do
|
|||
end
|
||||
|
||||
@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.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(:auth, OAuth.Authorization.delete_by_user_query(user))
|
||||
|
||||
|
@ -530,7 +621,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
end
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
{nil, false} ->
|
||||
case Utils.fetch_latest_follow(follower, following) do
|
||||
|
@ -832,10 +937,6 @@ def get_or_fetch_by_nickname(nickname) do
|
|||
_e ->
|
||||
with [_nick, _domain] <- String.split(nickname, "@"),
|
||||
{:ok, user} <- fetch_by_nickname(nickname) do
|
||||
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
|
||||
fetch_initial_posts(user)
|
||||
end
|
||||
|
||||
{:ok, user}
|
||||
else
|
||||
_e -> {:error, "not found " <> nickname}
|
||||
|
@ -843,11 +944,6 @@ def get_or_fetch_by_nickname(nickname) do
|
|||
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()
|
||||
def get_followers_query(%User{} = user, nil) do
|
||||
User.Query.build(%{followers: user, deactivated: false})
|
||||
|
@ -1215,13 +1311,15 @@ def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
|
||||
E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
|
||||
Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
|
||||
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())}
|
||||
def outgoing_relations_ap_ids(_, []), do: %{}
|
||||
@spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
|
||||
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
|
||||
db_result =
|
||||
user
|
||||
|
@ -1240,6 +1338,30 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types)
|
|||
)
|
||||
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
|
||||
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
|
||||
end
|
||||
|
@ -1313,16 +1435,6 @@ def perform(:delete, %User{} = user) do
|
|||
Repo.delete(user)
|
||||
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)
|
||||
|
||||
@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
|
||||
{:ok, user}
|
||||
else
|
||||
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
|
||||
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
|
||||
fetch_by_ap_id(ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1671,8 +1772,12 @@ def all_superusers do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def muting_reblogs?(%User{} = user, %User{} = target) do
|
||||
UserRelationship.reblog_mute_exists?(user, target)
|
||||
end
|
||||
|
||||
def showing_reblogs?(%User{} = user, %User{} = target) do
|
||||
not UserRelationship.reblog_mute_exists?(user, target)
|
||||
not muting_reblogs?(user, target)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -1878,6 +1983,17 @@ def fields(%{fields: nil}), do: []
|
|||
|
||||
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
|
||||
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
|
||||
limit = Pleroma.Config.get([:instance, limit_name], 0)
|
||||
|
@ -2055,4 +2171,27 @@ def set_invisible(user, invisible) do
|
|||
|> validate_required([:invisible])
|
||||
|> update_and_set_cache()
|
||||
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
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
|
|||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
|
@ -21,19 +22,26 @@ defmodule Pleroma.UserRelationship do
|
|||
end
|
||||
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
do: exists?(unquote(relationship_type), source, target)
|
||||
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
|
||||
user_relationship
|
||||
|> cast(params, [:relationship_type, :source_id, :target_id])
|
||||
|
@ -72,6 +80,73 @@ def delete(relationship_type, %User{} = source, %User{} = target) do
|
|||
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
|
||||
changeset
|
||||
|> validate_change(:target_id, fn _, target_id ->
|
||||
|
|
|
@ -503,8 +503,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
|||
defp do_follow(follower, followed, activity_id, local) do
|
||||
with data <- make_follow_data(follower, followed, activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
|
@ -584,6 +583,16 @@ defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options)
|
|||
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()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
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
|
||||
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_relations ++
|
||||
ap_id_relationships =
|
||||
ap_id_relationships ++
|
||||
if opts["blocking_user"] && opts["blocking_user"] == source_user do
|
||||
[:block]
|
||||
else
|
||||
[]
|
||||
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_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
alias Pleroma.Delivery
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Plugs.EnsureAuthenticatedPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
|
@ -18,23 +19,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.FederatingPlug
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
require Logger
|
||||
|
||||
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(
|
||||
Pleroma.Plugs.Cache,
|
||||
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
||||
when action in [:activity, :object]
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
|
||||
plug(:set_requester_reachable when action in [:inbox])
|
||||
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
|
||||
conn
|
||||
else
|
||||
|
@ -127,11 +142,13 @@ defp set_cache_ttl_for(conn, entity) do
|
|||
end
|
||||
|
||||
# GET /relay/following
|
||||
def following(%{assigns: %{relay: true}} = conn, _params) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("following.json", %{user: Relay.get_actor()})
|
||||
def relay_following(conn, _params) do
|
||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("following.json", %{user: Relay.get_actor()})
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
# GET /relay/followers
|
||||
def followers(%{assigns: %{relay: true}} = conn, _params) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("followers.json", %{user: Relay.get_actor()})
|
||||
def relay_followers(conn, _params) do
|
||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("followers.json", %{user: Relay.get_actor()})
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- User.ensure_keys_present(user) do
|
||||
activities =
|
||||
if params["max_id"] do
|
||||
ActivityPub.fetch_user_activities(user, nil, %{
|
||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
||||
"max_id" => params["max_id"],
|
||||
# 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
|
||||
|
@ -214,7 +236,7 @@ def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
|
|||
"limit" => 10
|
||||
})
|
||||
else
|
||||
ActivityPub.fetch_user_activities(user, nil, %{
|
||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
||||
"limit" => 10,
|
||||
"include_poll_votes" => true
|
||||
})
|
||||
|
@ -255,8 +277,16 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
|
|||
json(conn, "ok")
|
||||
end
|
||||
|
||||
# only accept relayed Creates
|
||||
def inbox(conn, %{"type" => "Create"} = params) do
|
||||
# POST /relay/inbox -or- POST /internal/fetch/inbox
|
||||
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(
|
||||
"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")
|
||||
end
|
||||
|
||||
def inbox(conn, params) do
|
||||
defp post_inbox_fallback(conn, params) do
|
||||
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(
|
||||
"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))
|
||||
end
|
||||
|
||||
json(conn, dgettext("errors", "error"))
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(dgettext("errors", "error"))
|
||||
end
|
||||
|
||||
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})
|
||||
end
|
||||
|
||||
def whoami(_conn, _params), do: {:error, :not_found}
|
||||
|
||||
def read_inbox(
|
||||
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
|
||||
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
||||
%{"nickname" => nickname, "page" => page?} = params
|
||||
)
|
||||
when page? in [true, "true"] do
|
||||
|
@ -337,7 +368,7 @@ def read_inbox(
|
|||
})
|
||||
end
|
||||
|
||||
def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
|
||||
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
|
||||
"nickname" => nickname
|
||||
}) 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
|
||||
|
||||
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
|
||||
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, %{
|
||||
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
||||
"nickname" => nickname
|
||||
}) do
|
||||
err =
|
||||
|
@ -370,7 +393,7 @@ def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
|
|||
|> json(err)
|
||||
end
|
||||
|
||||
def handle_user_activity(user, %{"type" => "Create"} = params) do
|
||||
defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
|
||||
object =
|
||||
params["object"]
|
||||
|> Map.merge(Map.take(params, ["to", "cc"]))
|
||||
|
@ -386,7 +409,7 @@ def handle_user_activity(user, %{"type" => "Create"} = params) do
|
|||
})
|
||||
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"]),
|
||||
true <- user.is_moderator || user.ap_id == object.data["actor"],
|
||||
{:ok, delete} <- ActivityPub.delete(object) do
|
||||
|
@ -396,7 +419,7 @@ def handle_user_activity(user, %{"type" => "Delete"} = params) do
|
|||
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"]),
|
||||
{:ok, activity, _object} <- ActivityPub.like(user, object) do
|
||||
{:ok, activity}
|
||||
|
@ -405,7 +428,7 @@ def handle_user_activity(user, %{"type" => "Like"} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_user_activity(_, _) do
|
||||
defp handle_user_activity(_, _) do
|
||||
{:error, dgettext("errors", "Unhandled activity type")}
|
||||
end
|
||||
|
||||
|
@ -434,7 +457,7 @@ def update_outbox(
|
|||
end
|
||||
end
|
||||
|
||||
def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
|
||||
def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
|
||||
err =
|
||||
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
|
||||
nickname: nickname,
|
||||
|
@ -446,13 +469,13 @@ def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} =
|
|||
|> json(err)
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
defp errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(dgettext("errors", "Not found"))
|
||||
end
|
||||
|
||||
def errors(conn, _e) do
|
||||
defp errors(conn, _e) do
|
||||
conn
|
||||
|> put_status(:internal_server_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 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} <-
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
|
|
|
@ -60,15 +60,28 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
|||
|
||||
def publish(_), do: {:error, "Not implemented"}
|
||||
|
||||
@spec list() :: {:ok, [String.t()]} | {:error, any()}
|
||||
def list do
|
||||
@spec list(boolean()) :: {:ok, [String.t()]} | {:error, any()}
|
||||
def list(with_not_accepted \\ false) do
|
||||
with %User{} = user <- get_actor() do
|
||||
list =
|
||||
accepted =
|
||||
user
|
||||
|> User.following()
|
||||
|> Enum.map(fn entry -> URI.parse(entry).host end)
|
||||
|> 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}
|
||||
else
|
||||
error -> format_error(error)
|
||||
|
|
|
@ -1108,13 +1108,11 @@ def add_hashtags(object) do
|
|||
end
|
||||
|
||||
def add_mention_tags(object) do
|
||||
mentions =
|
||||
object
|
||||
|> Utils.get_notified_from_object()
|
||||
|> Enum.map(&build_mention_tag/1)
|
||||
{enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
|
||||
potential_receivers = enabled_receivers ++ disabled_receivers
|
||||
mentions = Enum.map(potential_receivers, &build_mention_tag/1)
|
||||
|
||||
tags = object["tag"] || []
|
||||
|
||||
Map.put(object, "tag", tags ++ mentions)
|
||||
end
|
||||
|
||||
|
|
|
@ -440,22 +440,19 @@ def update_follow_state_for_all(
|
|||
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|
||||
|> Repo.update_all([])
|
||||
|
||||
User.set_follow_state_cache(actor, object, state)
|
||||
|
||||
activity = Activity.get_by_id(activity.id)
|
||||
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def update_follow_state(
|
||||
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
|
||||
%Activity{} = activity,
|
||||
state
|
||||
) do
|
||||
new_data = Map.put(activity.data, "state", state)
|
||||
changeset = Changeset.change(activity, data: new_data)
|
||||
|
||||
with {:ok, activity} <- Repo.update(changeset) do
|
||||
User.set_follow_state_cache(actor, object, state)
|
||||
{:ok, activity}
|
||||
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: []
|
||||
|
||||
@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
|
||||
def get_reports(params, page, page_size) do
|
||||
params =
|
||||
|
|
|
@ -73,6 +73,7 @@ def render("user.json", %{user: user}) do
|
|||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
user = User.sanitize_html(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
|
@ -81,12 +82,6 @@ def render("user.json", %{user: user}) do
|
|||
fields =
|
||||
user
|
||||
|> 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"))
|
||||
|
||||
%{
|
||||
|
|
|
@ -44,6 +44,7 @@ def is_direct?(activity) do
|
|||
def is_list?(%{data: %{"listMessage" => _}}), do: true
|
||||
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?(%{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?(activity, nil) do
|
||||
is_public?(activity)
|
||||
def visible_for_user?(%{local: local} = activity, nil) do
|
||||
cfg_key =
|
||||
if local,
|
||||
do: :local,
|
||||
else: :remote
|
||||
|
||||
if Pleroma.Config.get([:restrict_unauthenticated, :activities, cfg_key]),
|
||||
do: false,
|
||||
else: is_public?(activity)
|
||||
end
|
||||
|
||||
def visible_for_user?(activity, user) do
|
||||
x = [user.ap_id | User.following(user)]
|
||||
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
|
||||
|
||||
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
||||
|
|
|
@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{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(
|
||||
|
@ -54,7 +54,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
:tag_users,
|
||||
:untag_users,
|
||||
: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, "")
|
||||
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
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
|
@ -745,14 +792,14 @@ def report_notes_delete(%{assigns: %{user: user}} = conn, %{
|
|||
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
|
||||
local_only = params["local_only"] == "true" || params["local_only"] == true
|
||||
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_statuses(admin, %{
|
||||
ActivityPub.fetch_statuses(nil, %{
|
||||
"godmode" => godmode,
|
||||
"local_only" => local_only,
|
||||
"limit" => page_size,
|
||||
|
@ -834,7 +881,7 @@ def config_show(conn, _params) do
|
|||
configs = ConfigDB.get_all_as_keyword()
|
||||
|
||||
merged =
|
||||
Config.Holder.config()
|
||||
Config.Holder.default_config()
|
||||
|> ConfigDB.merge(configs)
|
||||
|> Enum.map(fn {group, values} ->
|
||||
Enum.map(values, fn {key, value} ->
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Pleroma.Web.AdminAPI.AccountView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
@ -24,9 +23,47 @@ def render("index.json", %{users: users}) do
|
|||
}
|
||||
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
|
||||
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,
|
||||
|
@ -104,4 +141,7 @@ defp parse_error(errors) do
|
|||
""
|
||||
end
|
||||
end
|
||||
|
||||
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
|
||||
defp image_url(_), do: nil
|
||||
end
|
||||
|
|
|
@ -358,7 +358,7 @@ def remove_mute(user, activity) do
|
|||
def thread_muted?(%{id: nil} = _user, _activity), do: false
|
||||
|
||||
def thread_muted?(user, activity) do
|
||||
ThreadMute.check_muted(user.id, activity.data["context"]) != []
|
||||
ThreadMute.exists?(user.id, activity.data["context"])
|
||||
end
|
||||
|
||||
def report(user, %{"account_id" => account_id} = data) do
|
||||
|
|
|
@ -331,7 +331,7 @@ def format_input(text, "text/html", options) do
|
|||
def format_input(text, "text/markdown", options) do
|
||||
text
|
||||
|> Formatter.mentions_escape(options)
|
||||
|> Earmark.as_html!()
|
||||
|> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer})
|
||||
|> Formatter.linkify(options)
|
||||
|> Formatter.html_escape("text/html")
|
||||
end
|
||||
|
@ -591,7 +591,7 @@ def validate_character_limit(full_payload, _attachments) do
|
|||
limit = Pleroma.Config.get([:instance, :limit])
|
||||
length = String.length(full_payload)
|
||||
|
||||
if length < limit do
|
||||
if length <= limit do
|
||||
:ok
|
||||
else
|
||||
{:error, dgettext("errors", "The status is over the character limit")}
|
||||
|
|
|
@ -34,7 +34,12 @@ defp param_to_integer(val, default) when is_binary(val) do
|
|||
|
||||
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
|
||||
%{id: max_id} ->
|
||||
params =
|
||||
|
@ -87,7 +92,8 @@ def try_render(conn, _, _) do
|
|||
render_error(conn, :not_implemented, "Can't display this activity")
|
||||
end
|
||||
|
||||
@spec put_in_if_exist(map(), atom() | String.t(), any) :: map()
|
||||
def put_in_if_exist(map, _key, nil), do: map
|
||||
def put_in_if_exist(map, key, value), do: put_in(map, key, value)
|
||||
@spec put_if_exist(map(), atom() | String.t(), any) :: map()
|
||||
def put_if_exist(map, _key, nil), do: map
|
||||
|
||||
def put_if_exist(map, key, value), do: Map.put(map, key, value)
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Pleroma.Plugs.HTTPSecurityPlug)
|
||||
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
|
||||
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
|
||||
|
|
|
@ -9,18 +9,18 @@ defmodule Pleroma.Web.Feed.TagController do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
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
|
||||
{format, tag} = parse_tag(raw_tag)
|
||||
|
||||
activities =
|
||||
%{"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()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> put_resp_content_type("application/#{format}+xml")
|
||||
|> put_view(FeedView)
|
||||
|> render("tag.#{format}",
|
||||
activities: activities,
|
||||
|
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.Feed.UserController do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPubController
|
||||
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])
|
||||
|
||||
|
@ -25,7 +25,12 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname
|
|||
|
||||
def feed_redirect(%{assigns: %{format: format}} = conn, _params)
|
||||
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
|
||||
|
||||
def feed_redirect(conn, %{"nickname" => nickname}) do
|
||||
|
@ -35,19 +40,28 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
|
||||
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
|
||||
activities =
|
||||
%{
|
||||
"type" => ["Create"],
|
||||
"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()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> put_resp_content_type("application/#{format}+xml")
|
||||
|> put_view(FeedView)
|
||||
|> render("user.xml",
|
||||
|> render("user.#{format}",
|
||||
user: user,
|
||||
activities: activities,
|
||||
feed_config: Pleroma.Config.get([:feed])
|
||||
|
|
|
@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
import Pleroma.Web.ControllerHelper,
|
||||
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.RateLimiter
|
||||
alias Pleroma.User
|
||||
|
@ -60,14 +59,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
plug(
|
||||
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
|
||||
|
||||
plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
|
||||
plug(RateLimiter, [name: :relations_actions] when action in @relations)
|
||||
plug(
|
||||
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(:assign_account_by_id when action in @needs_account)
|
||||
|
||||
|
@ -76,7 +79,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
@doc "POST /api/v1/accounts"
|
||||
def create(
|
||||
%{assigns: %{app: app}} = conn,
|
||||
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
|
||||
%{"username" => nickname, "password" => _, "agreement" => true} = params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|
@ -93,7 +96,8 @@ def create(
|
|||
|> Map.put("bio", params["bio"] || "")
|
||||
|> 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
|
||||
json(conn, %{
|
||||
token_type: "Bearer",
|
||||
|
@ -114,6 +118,15 @@ def create(conn, _) do
|
|||
render_error(conn, :forbidden, "Invalid credentials")
|
||||
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"
|
||||
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
|
||||
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
|
||||
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 =
|
||||
[
|
||||
: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)})
|
||||
end)
|
||||
|> 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, "avatar", :avatar, fn value ->
|
||||
with %Plug.Upload{} <- value,
|
||||
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
|
||||
{:ok, object.data}
|
||||
end
|
||||
end)
|
||||
|> add_if_present(params, "header", :banner, fn value ->
|
||||
with %Plug.Upload{} <- value,
|
||||
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
|
||||
{:ok, object.data}
|
||||
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, "note", :bio)
|
||||
|> add_if_present(params, "avatar", :avatar)
|
||||
|> add_if_present(params, "header", :banner)
|
||||
|> add_if_present(params, "pleroma_background_image", :background)
|
||||
|> add_if_present(
|
||||
params,
|
||||
"fields_attributes",
|
||||
:raw_fields,
|
||||
&{:ok, normalize_fields_attributes(&1)}
|
||||
)
|
||||
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
|
||||
|> add_if_present(params, "default_scope", :default_scope)
|
||||
|> 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)
|
||||
|
||||
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"
|
||||
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
|
||||
|> Map.put("tag", params["tagged"])
|
||||
|
@ -261,6 +238,8 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
|||
|> add_link_headers(activities)
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", activities: activities, for: reading_user, as: :activity)
|
||||
else
|
||||
_e -> render_error(conn, :not_found, "Can't find user")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -86,6 +86,6 @@ defp local_mastodon_root_path(conn) do
|
|||
@spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp get_or_make_app do
|
||||
%{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
|
||||
|
|
|
@ -76,7 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
%{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
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
|
||||
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)
|
||||
|
||||
|
@ -75,17 +75,30 @@ def direct(%{assigns: %{user: user}} = conn, params) do
|
|||
def public(%{assigns: %{user: user}} = conn, params) do
|
||||
local_only = truthy_param?(params["local"])
|
||||
|
||||
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()
|
||||
cfg_key =
|
||||
if local_only do
|
||||
:local
|
||||
else
|
||||
:federated
|
||||
end
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities, %{"local" => local_only})
|
||||
|> render("index.json", activities: activities, for: user, as: :activity)
|
||||
restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
|
||||
|
||||
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
|
||||
|
||||
def hashtag_fetching(params, user, local_only) do
|
||||
|
|
|
@ -55,6 +55,7 @@ def get_notifications(user, params \\ %{}) do
|
|||
|
||||
user
|
||||
|> Notification.for_user_query(options)
|
||||
|> restrict(:include_types, options)
|
||||
|> restrict(:exclude_types, options)
|
||||
|> restrict(:account_ap_id, options)
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
@ -69,10 +70,10 @@ def get_scheduled_activities(user, params \\ %{}) do
|
|||
defp cast_params(params) do
|
||||
param_types = %{
|
||||
exclude_types: {:array, :string},
|
||||
include_types: {:array, :string},
|
||||
exclude_visibilities: {:array, :string},
|
||||
reblogs: :boolean,
|
||||
with_muted: :boolean,
|
||||
with_move: :boolean,
|
||||
account_ap_id: :string
|
||||
}
|
||||
|
||||
|
@ -80,14 +81,16 @@ defp cast_params(params) do
|
|||
changeset.changes
|
||||
end
|
||||
|
||||
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
|
||||
ap_types =
|
||||
mastodon_types
|
||||
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|
||||
|> Enum.filter(& &1)
|
||||
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
|
||||
ap_types = convert_and_filter_mastodon_types(mastodon_types)
|
||||
|
||||
query
|
||||
|> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
where(query, [q, a], 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -5,13 +5,28 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.AccountView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
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
|
||||
|> render_many(AccountView, "show.json", opts)
|
||||
|> Enum.filter(&Enum.any?/1)
|
||||
|
@ -36,37 +51,111 @@ def render("relationship.json", %{user: nil, target: _target}) do
|
|||
%{}
|
||||
end
|
||||
|
||||
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
|
||||
follow_state = User.get_cached_follow_state(user, target)
|
||||
def render(
|
||||
"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 =
|
||||
if follow_state && !User.following?(user, target) do
|
||||
follow_state == "pending"
|
||||
follow_state =
|
||||
if following_relationships do
|
||||
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
|
||||
false
|
||||
User.get_follow_state(reading_user, target)
|
||||
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),
|
||||
following: User.following?(user, target),
|
||||
followed_by: User.following?(target, user),
|
||||
blocking: User.blocks_user?(user, target),
|
||||
blocked_by: User.blocks_user?(target, user),
|
||||
muting: User.mutes?(user, target),
|
||||
muting_notifications: User.muted_notifications?(user, target),
|
||||
subscribing: User.subscribed_to?(user, target),
|
||||
requested: requested,
|
||||
domain_blocking: User.blocks_domain?(user, target),
|
||||
showing_reblogs: User.showing_reblogs?(user, target),
|
||||
following: follow_state == "accept",
|
||||
followed_by: followed_by,
|
||||
blocking:
|
||||
UserRelationship.exists?(
|
||||
user_relationships,
|
||||
:block,
|
||||
reading_user,
|
||||
target,
|
||||
&User.blocks_user?(&1, &2)
|
||||
),
|
||||
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
|
||||
}
|
||||
end
|
||||
|
||||
def render("relationships.json", %{user: user, targets: targets}) do
|
||||
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
|
||||
def render("relationships.json", %{user: user, targets: targets} = 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(user, targets)
|
||||
end
|
||||
|
||||
render_opts = %{as: :target, user: user, relationships: relationships_opt}
|
||||
render_many(targets, AccountView, "relationship.json", render_opts)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
|
@ -100,18 +189,12 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
}
|
||||
end)
|
||||
|
||||
fields =
|
||||
user
|
||||
|> User.fields()
|
||||
|> Enum.map(fn %{"name" => name, "value" => value} ->
|
||||
%{
|
||||
"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})
|
||||
relationship =
|
||||
render("relationship.json", %{
|
||||
user: opts[:for],
|
||||
target: user,
|
||||
relationships: opts[:relationships]
|
||||
})
|
||||
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
|
@ -123,17 +206,17 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
followers_count: followers_count,
|
||||
following_count: following_count,
|
||||
statuses_count: user.note_count,
|
||||
note: bio || "",
|
||||
note: user.bio || "",
|
||||
url: User.profile_url(user),
|
||||
avatar: image,
|
||||
avatar_static: image,
|
||||
header: header,
|
||||
header_static: header,
|
||||
emojis: emojis,
|
||||
fields: fields,
|
||||
fields: user.fields,
|
||||
bot: bot,
|
||||
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,
|
||||
fields: user.raw_fields,
|
||||
pleroma: %{
|
||||
|
|
|
@ -8,24 +8,86 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("index.json", %{notifications: notifications, for: user}) do
|
||||
safe_render_many(notifications, NotificationView, "show.json", %{for: user})
|
||||
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
|
||||
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
|
||||
|
||||
def render("show.json", %{
|
||||
notification: %Notification{activity: activity} = notification,
|
||||
for: user
|
||||
}) do
|
||||
def render(
|
||||
"show.json",
|
||||
%{
|
||||
notification: %Notification{activity: activity} = notification,
|
||||
for: reading_user
|
||||
} = opts
|
||||
) do
|
||||
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)
|
||||
|
||||
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 = %{
|
||||
id: to_string(notification.id),
|
||||
type: mastodon_type,
|
||||
|
@ -36,24 +98,28 @@ def render("show.json", %{
|
|||
}
|
||||
}
|
||||
|
||||
render_opts = %{relationships: opts[:relationships]}
|
||||
|
||||
case mastodon_type do
|
||||
"mention" ->
|
||||
put_status(response, activity, user)
|
||||
put_status(response, activity, reading_user, render_opts)
|
||||
|
||||
"favourite" ->
|
||||
put_status(response, parent_activity, user)
|
||||
put_status(response, parent_activity_fn.(), reading_user, render_opts)
|
||||
|
||||
"reblog" ->
|
||||
put_status(response, parent_activity, user)
|
||||
put_status(response, parent_activity_fn.(), reading_user, render_opts)
|
||||
|
||||
"move" ->
|
||||
put_target(response, activity, user)
|
||||
put_target(response, activity, reading_user, render_opts)
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
"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
|
||||
|
@ -64,16 +130,21 @@ def render("show.json", %{
|
|||
end
|
||||
|
||||
defp put_emoji(response, activity) do
|
||||
response
|
||||
|> Map.put(:emoji, activity.data["content"])
|
||||
Map.put(response, :emoji, activity.data["content"])
|
||||
end
|
||||
|
||||
defp put_status(response, activity, user) do
|
||||
Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
|
||||
defp put_status(response, activity, reading_user, opts) do
|
||||
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
|
||||
|
||||
defp put_target(response, activity, user) do
|
||||
target = User.get_cached_by_ap_id(activity.data["target"])
|
||||
Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
|
||||
defp put_target(response, activity, reading_user, opts) do
|
||||
target_user = User.get_cached_by_ap_id(activity.data["target"])
|
||||
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
|
||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
|
@ -71,10 +72,41 @@ defp reblogged?(activity, user) do
|
|||
end
|
||||
|
||||
def render("index.json", opts) do
|
||||
replied_to_activities = get_replied_to_activities(opts.activities)
|
||||
opts = Map.put(opts, :replied_to_activities, replied_to_activities)
|
||||
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
|
||||
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
|
||||
|
||||
def render(
|
||||
|
@ -85,17 +117,25 @@ def render(
|
|||
created_at = Utils.to_masto_date(activity.data["published"])
|
||||
activity_object = Object.normalize(activity)
|
||||
|
||||
reblogged_activity =
|
||||
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()
|
||||
reblogged_parent_activity =
|
||||
if opts[:parent_activities] do
|
||||
Activity.Queries.find_by_object_ap_id(
|
||||
opts[:parent_activities],
|
||||
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"] || [])
|
||||
|
||||
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
|
||||
bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
|
||||
|
||||
mentions =
|
||||
activity.recipients
|
||||
|
@ -107,7 +147,12 @@ def render(
|
|||
id: to_string(activity.id),
|
||||
uri: 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_account_id: nil,
|
||||
reblog: reblogged,
|
||||
|
@ -116,7 +161,7 @@ def render(
|
|||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
favourites_count: 0,
|
||||
reblogged: reblogged?(reblogged_activity, opts[:for]),
|
||||
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
|
||||
favourited: present?(favorited),
|
||||
bookmarked: present?(bookmarked),
|
||||
muted: false,
|
||||
|
@ -183,9 +228,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
end
|
||||
|
||||
thread_muted? =
|
||||
case activity.thread_muted? do
|
||||
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
|
||||
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
|
||||
cond do
|
||||
is_nil(opts[:for]) -> false
|
||||
is_boolean(activity.thread_muted?) -> activity.thread_muted?
|
||||
true -> CommonAPI.thread_muted?(opts[:for], activity)
|
||||
end
|
||||
|
||||
attachment_data = object.data["attachment"] || []
|
||||
|
@ -253,11 +299,26 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
_ -> []
|
||||
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),
|
||||
uri: object.data["id"],
|
||||
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_account_id: reply_to_user && to_string(reply_to_user.id),
|
||||
reblog: nil,
|
||||
|
@ -270,7 +331,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
reblogged: reblogged?(activity, opts[:for]),
|
||||
favourited: present?(favorited),
|
||||
bookmarked: present?(bookmarked),
|
||||
muted: thread_muted? || User.mutes?(opts[:for], user),
|
||||
muted: muted,
|
||||
pinned: pinned?(activity, user),
|
||||
sensitive: sensitive,
|
||||
spoiler_text: summary,
|
||||
|
|
|
@ -60,6 +60,7 @@ def raw_nodeinfo do
|
|||
"pleroma_explicit_addressing",
|
||||
"shareable_emoji_packs",
|
||||
"multifetch",
|
||||
"pleroma:api/v1/notifications:include_types_filter",
|
||||
if Config.get([:media_proxy, :enabled]) do
|
||||
"media_proxy"
|
||||
end,
|
||||
|
|
|
@ -16,6 +16,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
alias Pleroma.Web.Metadata.PlayerView
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
|
||||
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
|
||||
)
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
|
||||
|
@ -135,13 +139,13 @@ def notice_player(conn, %{"id" => id}) do
|
|||
end
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
defp errors(conn, {:error, :not_found}) do
|
||||
render_error(conn, :not_found, "Not found")
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -101,6 +101,11 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id})
|
|||
conn
|
||||
|> put_view(ConversationView)
|
||||
|> render("participation.json", %{participation: participation, for: user})
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{"error" => "Unknown conversation id"})
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -108,9 +113,9 @@ def conversation_statuses(
|
|||
%{assigns: %{user: user}} = conn,
|
||||
%{"id" => participation_id} = params
|
||||
) do
|
||||
participation = Participation.get(participation_id, preload: [:conversation])
|
||||
|
||||
if user.id == participation.user_id do
|
||||
with %Participation{} = participation <-
|
||||
Participation.get(participation_id, preload: [:conversation]),
|
||||
true <- user.id == participation.user_id do
|
||||
params =
|
||||
params
|
||||
|> Map.put("blocking_user", user)
|
||||
|
@ -126,6 +131,11 @@ def conversation_statuses(
|
|||
|> add_link_headers(activities)
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, for: user, as: :activity})
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{"error" => "Unknown conversation id"})
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -133,15 +143,22 @@ def update_conversation(
|
|||
%{assigns: %{user: user}} = conn,
|
||||
%{"id" => participation_id, "recipients" => recipients}
|
||||
) do
|
||||
participation =
|
||||
participation_id
|
||||
|> Participation.get()
|
||||
|
||||
with true <- user.id == participation.user_id,
|
||||
with %Participation{} = participation <- Participation.get(participation_id),
|
||||
true <- user.id == participation.user_id,
|
||||
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
|
||||
conn
|
||||
|> put_view(ConversationView)
|
||||
|> 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
|
||||
|
||||
|
|
|
@ -173,6 +173,8 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/users/:nickname/password_reset", AdminAPIController, :get_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/:nickname", AdminAPIController, :user_show)
|
||||
|
@ -513,7 +515,7 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
pipeline :ostatus do
|
||||
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
|
||||
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
|
||||
plug(Pleroma.Plugs.StaticFEPlug)
|
||||
end
|
||||
|
||||
|
@ -541,6 +543,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
|
||||
end
|
||||
|
||||
# Server to Server (S2S) AP interactions
|
||||
pipeline :activitypub do
|
||||
plug(:accepts, ["activity+json", "json"])
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
|
@ -554,6 +557,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||
end
|
||||
|
||||
# Client to Server (C2S) AP interactions
|
||||
pipeline :activitypub_client do
|
||||
plug(:accepts, ["activity+json", "json"])
|
||||
plug(:fetch_session)
|
||||
|
@ -597,8 +601,8 @@ defmodule Pleroma.Web.Router do
|
|||
post("/inbox", ActivityPubController, :inbox)
|
||||
end
|
||||
|
||||
get("/following", ActivityPubController, :following, assigns: %{relay: true})
|
||||
get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
|
||||
get("/following", ActivityPubController, :relay_following)
|
||||
get("/followers", ActivityPubController, :relay_followers)
|
||||
end
|
||||
|
||||
scope "/internal/fetch", Pleroma.Web.ActivityPub do
|
||||
|
|
|
@ -17,6 +17,10 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
|||
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
|
||||
plug(:assign_id)
|
||||
|
||||
plug(Pleroma.Plugs.EnsureAuthenticatedPlug,
|
||||
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
|
||||
)
|
||||
|
||||
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
||||
|
||||
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: ""})
|
||||
end
|
||||
|
||||
def get_counts(%Activity{} = activity) do
|
||||
defp get_counts(%Activity{} = activity) do
|
||||
%Object{data: data} = Object.normalize(activity)
|
||||
|
||||
%{
|
||||
|
@ -43,9 +47,9 @@ def get_counts(%Activity{} = activity) do
|
|||
}
|
||||
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"])
|
||||
|
||||
link =
|
||||
|
@ -54,10 +58,19 @@ def represent(%Activity{object: %Object{data: data}} = activity, selected) do
|
|||
_ -> data["url"] || data["external_url"] || data["id"]
|
||||
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),
|
||||
content: data["content"] || nil,
|
||||
content: content,
|
||||
attachment: data["attachment"],
|
||||
link: link,
|
||||
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
|
||||
|
||||
render(conn, "profile.html", %{
|
||||
user: user,
|
||||
user: User.sanitize_html(user),
|
||||
timeline: timeline,
|
||||
prev_page_id: prev_page_id,
|
||||
next_page_id: next_page_id,
|
||||
|
@ -147,17 +160,17 @@ def show(%{assigns: %{activity_id: _}} = conn, _params) do
|
|||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def assign_id(conn, _opts), do: conn
|
||||
defp assign_id(conn, _opts), do: conn
|
||||
end
|
||||
|
|
|
@ -130,7 +130,7 @@ defp do_stream(%{topic: topic, item: item}) do
|
|||
|
||||
defp should_send?(%User{} = user, %Activity{} = item) do
|
||||
%{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)
|
||||
recipients = MapSet.new(item.recipients)
|
||||
|
|
49
lib/pleroma/web/templates/feed/feed/_activity.rss.eex
Normal file
49
lib/pleroma/web/templates/feed/feed/_activity.rss.eex
Normal 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>
|
17
lib/pleroma/web/templates/feed/feed/_author.rss.eex
Normal file
17
lib/pleroma/web/templates/feed/feed/_author.rss.eex
Normal 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>
|
|
@ -12,13 +12,13 @@
|
|||
<logo><%= logo(@user) %></logo>
|
||||
<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 %>
|
||||
<link rel="next" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
|
||||
<% end %>
|
||||
|
||||
<%= 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 %>
|
||||
</feed>
|
20
lib/pleroma/web/templates/feed/feed/user.rss.eex
Normal file
20
lib/pleroma/web/templates/feed/feed/user.rss.eex
Normal 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>
|
|
@ -16,6 +16,8 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
|
|||
|
||||
@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)
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
|
|
|
@ -17,6 +17,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|
|||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.WebFinger
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["follow", "write:follows"]}
|
||||
|
|
|
@ -10,10 +10,6 @@ defmodule Pleroma.Workers.BackgroundWorker do
|
|||
use Pleroma.Workers.WorkerHelper, queue: "background"
|
||||
|
||||
@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
|
||||
user = User.get_cached_by_id(user_id)
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
|||
def project do
|
||||
[
|
||||
app: :pleroma,
|
||||
version: version("1.1.50"),
|
||||
version: version("2.0.50"),
|
||||
elixir: "~> 1.8",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
|
@ -63,7 +63,7 @@ def copy_nginx_config(%{path: target_path} = release) do
|
|||
def application do
|
||||
[
|
||||
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]
|
||||
]
|
||||
end
|
||||
|
@ -126,7 +126,7 @@ defp deps do
|
|||
{:ex_aws_s3, "~> 2.0"},
|
||||
{:sweet_xml, "~> 0.6.6"},
|
||||
{:earmark, "~> 1.3"},
|
||||
{:bbcode, "~> 0.1.1"},
|
||||
{:bbcode_pleroma, "~> 0.2.0"},
|
||||
{:ex_machina, "~> 2.3", only: :test},
|
||||
{:credo, "~> 1.1.0", only: [:dev, :test], runtime: false},
|
||||
{:mock, "~> 0.3.3", only: :test},
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -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"},
|
||||
"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_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"},
|
||||
"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"},
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue