Merge remote-tracking branch 'remotes/origin/develop' into restricted-relations-embedding
This commit is contained in:
commit
b2924ab1fb
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -15,19 +15,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||||
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
||||||
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
||||||
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||||
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
||||||
- Mix task to create trusted OAuth App.
|
- Mix task to create trusted OAuth App.
|
||||||
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
|
- Notifications: Added `follow_request` notification type.
|
||||||
- Added `:reject_deletes` group to SimplePolicy
|
- Added `:reject_deletes` group to SimplePolicy
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
- Mastodon API: Extended `/api/v1/instance`.
|
||||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||||
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
||||||
- Mastodon API: Add support for filtering replies in public and home timelines
|
- Mastodon API: Add support for filtering replies in public and home timelines
|
||||||
- Admin API: endpoints for create/update/delete OAuth Apps.
|
- Admin API: endpoints for create/update/delete OAuth Apps.
|
||||||
|
- Admin API: endpoint for status view.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -35,12 +38,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
|
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
|
||||||
- Fix follower/blocks import when nicknames starts with @
|
- Fix follower/blocks import when nicknames starts with @
|
||||||
- Filtering of push notifications on activities from blocked domains
|
- Filtering of push notifications on activities from blocked domains
|
||||||
|
- Resolving Peertube accounts with Webfinger
|
||||||
|
|
||||||
## [unreleased-patch]
|
## [unreleased-patch]
|
||||||
|
### Security
|
||||||
|
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
|
||||||
|
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Logger configuration through AdminFE
|
- Logger configuration through AdminFE
|
||||||
- HTTP Basic Authentication permissions issue
|
- HTTP Basic Authentication permissions issue
|
||||||
- ObjectAgePolicy didn't filter out old messages
|
- ObjectAgePolicy didn't filter out old messages
|
||||||
|
- Transmogrifier: Keep object sensitive settings for outgoing representation (AP C2S)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
|
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
|
||||||
|
@ -147,6 +156,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Mastodon API: `pleroma.thread_muted` to the Status entity
|
- Mastodon API: `pleroma.thread_muted` to the Status entity
|
||||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||||
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
|
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
|
||||||
|
- Mastodon API: Add `pleroma.unread_count` to the Marker entity
|
||||||
- Admin API: Render whole status in grouped reports
|
- Admin API: Render whole status in grouped reports
|
||||||
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
||||||
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
|
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
|
||||||
|
|
|
@ -238,7 +238,18 @@
|
||||||
account_field_value_length: 2048,
|
account_field_value_length: 2048,
|
||||||
external_user_synchronization: true,
|
external_user_synchronization: true,
|
||||||
extended_nickname_format: true,
|
extended_nickname_format: true,
|
||||||
cleanup_attachments: false
|
cleanup_attachments: false,
|
||||||
|
multi_factor_authentication: [
|
||||||
|
totp: [
|
||||||
|
# digits 6 or 8
|
||||||
|
digits: 6,
|
||||||
|
period: 30
|
||||||
|
],
|
||||||
|
backup_codes: [
|
||||||
|
number: 5,
|
||||||
|
length: 16
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
config :pleroma, :feed,
|
config :pleroma, :feed,
|
||||||
post_title: %{
|
post_title: %{
|
||||||
|
@ -560,8 +571,6 @@
|
||||||
inactivity_threshold: 7
|
inactivity_threshold: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
config :pleroma, :notifications, enable_follow_request_notifications: false
|
|
||||||
|
|
||||||
config :pleroma, :oauth2,
|
config :pleroma, :oauth2,
|
||||||
token_expires_in: 600,
|
token_expires_in: 600,
|
||||||
issue_new_refresh_token: true,
|
issue_new_refresh_token: true,
|
||||||
|
@ -653,6 +662,8 @@
|
||||||
profiles: %{local: false, remote: false},
|
profiles: %{local: false, remote: false},
|
||||||
activities: %{local: false, remote: false}
|
activities: %{local: false, remote: false}
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
@ -919,6 +919,62 @@
|
||||||
key: :external_user_synchronization,
|
key: :external_user_synchronization,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Enabling following/followers counters synchronization for external users"
|
description: "Enabling following/followers counters synchronization for external users"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :multi_factor_authentication,
|
||||||
|
type: :keyword,
|
||||||
|
description: "Multi-factor authentication settings",
|
||||||
|
suggestions: [
|
||||||
|
[
|
||||||
|
totp: [digits: 6, period: 30],
|
||||||
|
backup_codes: [number: 5, length: 16]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :totp,
|
||||||
|
type: :keyword,
|
||||||
|
description: "TOTP settings",
|
||||||
|
suggestions: [digits: 6, period: 30],
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :digits,
|
||||||
|
type: :integer,
|
||||||
|
suggestions: [6],
|
||||||
|
description:
|
||||||
|
"Determines the length of a one-time pass-code, in characters. Defaults to 6 characters."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :period,
|
||||||
|
type: :integer,
|
||||||
|
suggestions: [30],
|
||||||
|
description:
|
||||||
|
"a period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :backup_codes,
|
||||||
|
type: :keyword,
|
||||||
|
description: "MFA backup codes settings",
|
||||||
|
suggestions: [number: 5, length: 16],
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :number,
|
||||||
|
type: :integer,
|
||||||
|
suggestions: [5],
|
||||||
|
description: "number of backup codes to generate."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :length,
|
||||||
|
type: :integer,
|
||||||
|
suggestions: [16],
|
||||||
|
description:
|
||||||
|
"Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2247,6 +2303,7 @@
|
||||||
children: [
|
children: [
|
||||||
%{
|
%{
|
||||||
key: :active,
|
key: :active,
|
||||||
|
label: "Enabled",
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Globally enable or disable digest emails"
|
description: "Globally enable or disable digest emails"
|
||||||
},
|
},
|
||||||
|
@ -2273,20 +2330,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
group: :pleroma,
|
|
||||||
key: :notifications,
|
|
||||||
type: :group,
|
|
||||||
description: "Notification settings",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :enable_follow_request_notifications,
|
|
||||||
type: :boolean,
|
|
||||||
description:
|
|
||||||
"Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: Pleroma.Emails.UserEmail,
|
key: Pleroma.Emails.UserEmail,
|
||||||
|
@ -3208,5 +3251,19 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: Pleroma.Web.ApiSpec.CastAndValidate,
|
||||||
|
type: :group,
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :strict,
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Enables strict input validation (useful in development, not recommended in production)",
|
||||||
|
suggestions: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -52,6 +52,8 @@
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
|
||||||
|
|
||||||
if File.exists?("./config/dev.secret.exs") do
|
if File.exists?("./config/dev.secret.exs") do
|
||||||
import_config "dev.secret.exs"
|
import_config "dev.secret.exs"
|
||||||
else
|
else
|
||||||
|
|
|
@ -56,6 +56,19 @@
|
||||||
ignore_hosts: [],
|
ignore_hosts: [],
|
||||||
ignore_tld: ["local", "localdomain", "lan"]
|
ignore_tld: ["local", "localdomain", "lan"]
|
||||||
|
|
||||||
|
config :pleroma, :instance,
|
||||||
|
multi_factor_authentication: [
|
||||||
|
totp: [
|
||||||
|
# digits 6 or 8
|
||||||
|
digits: 6,
|
||||||
|
period: 30
|
||||||
|
],
|
||||||
|
backup_codes: [
|
||||||
|
number: 2,
|
||||||
|
length: 6
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
config :web_push_encryption, :vapid_details,
|
config :web_push_encryption, :vapid_details,
|
||||||
subject: "mailto:administrator@example.com",
|
subject: "mailto:administrator@example.com",
|
||||||
public_key:
|
public_key:
|
||||||
|
@ -96,6 +109,8 @@
|
||||||
|
|
||||||
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
|
||||||
|
|
||||||
if File.exists?("./config/test.secret.exs") do
|
if File.exists?("./config/test.secret.exs") do
|
||||||
import_config "test.secret.exs"
|
import_config "test.secret.exs"
|
||||||
else
|
else
|
||||||
|
|
|
@ -409,6 +409,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
|
|
||||||
### Get a password reset token for a given nickname
|
### Get a password reset token for a given nickname
|
||||||
|
|
||||||
|
|
||||||
- Params: none
|
- Params: none
|
||||||
- Response:
|
- Response:
|
||||||
|
|
||||||
|
@ -427,6 +428,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- `nicknames`
|
- `nicknames`
|
||||||
- Response: none (code `204`)
|
- Response: none (code `204`)
|
||||||
|
|
||||||
|
## PUT `/api/pleroma/admin/users/disable_mfa`
|
||||||
|
|
||||||
|
### Disable mfa for user's account.
|
||||||
|
|
||||||
|
- Params:
|
||||||
|
- `nickname`
|
||||||
|
- Response: User’s nickname
|
||||||
|
|
||||||
## `GET /api/pleroma/admin/users/:nickname/credentials`
|
## `GET /api/pleroma/admin/users/:nickname/credentials`
|
||||||
|
|
||||||
### Get the user's email, password, display and settings-related fields
|
### Get the user's email, password, display and settings-related fields
|
||||||
|
@ -755,6 +764,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
||||||
- On success: `204`, empty response
|
- On success: `204`, empty response
|
||||||
|
|
||||||
|
## `GET /api/pleroma/admin/statuses/:id`
|
||||||
|
|
||||||
|
### Show status by id
|
||||||
|
|
||||||
|
- Params:
|
||||||
|
- `id`: required, status id
|
||||||
|
- Response:
|
||||||
|
- On failure:
|
||||||
|
- 404 Not Found `"Not Found"`
|
||||||
|
- On success: JSON, Mastodon Status entity
|
||||||
|
|
||||||
## `PUT /api/pleroma/admin/statuses/:id`
|
## `PUT /api/pleroma/admin/statuses/:id`
|
||||||
|
|
||||||
### Change the scope of an individual reported status
|
### Change the scope of an individual reported status
|
||||||
|
|
|
@ -61,6 +61,7 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `deactivated`: boolean, true when the user is deactivated
|
- `deactivated`: boolean, true when the user is deactivated
|
||||||
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
|
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
|
||||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||||
|
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
|
@ -204,3 +205,23 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
||||||
- `captcha_token`: optional, contains provider-specific captcha token
|
- `captcha_token`: optional, contains provider-specific captcha token
|
||||||
- `captcha_answer_data`: optional, contains provider-specific captcha data
|
- `captcha_answer_data`: optional, contains provider-specific captcha data
|
||||||
- `token`: invite token required when the registrations aren't public.
|
- `token`: invite token required when the registrations aren't public.
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
|
||||||
|
`GET /api/v1/instance` has additional fields
|
||||||
|
|
||||||
|
- `max_toot_chars`: The maximum characters per post
|
||||||
|
- `poll_limits`: The limits of polls
|
||||||
|
- `upload_limit`: The maximum upload file size
|
||||||
|
- `avatar_upload_limit`: The same for avatars
|
||||||
|
- `background_upload_limit`: The same for backgrounds
|
||||||
|
- `banner_upload_limit`: The same for banners
|
||||||
|
- `pleroma.metadata.features`: A list of supported features
|
||||||
|
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
||||||
|
- `vapid_public_key`: The public key needed for push messages
|
||||||
|
|
||||||
|
## Markers
|
||||||
|
|
||||||
|
Has these additional fields under the `pleroma` object:
|
||||||
|
|
||||||
|
- `unread_count`: contains number unread notifications
|
||||||
|
|
|
@ -70,7 +70,49 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
|
||||||
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
|
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
|
||||||
* Example response: `{"error": "Invalid password."}`
|
* Example response: `{"error": "Invalid password."}`
|
||||||
|
|
||||||
## `/api/pleroma/admin/`…
|
## `/api/pleroma/accounts/mfa`
|
||||||
|
#### Gets current MFA settings
|
||||||
|
* method: `GET`
|
||||||
|
* Authentication: required
|
||||||
|
* OAuth scope: `read:security`
|
||||||
|
* Response: JSON. Returns `{"enabled": "false", "totp": false }`
|
||||||
|
|
||||||
|
## `/api/pleroma/accounts/mfa/setup/totp`
|
||||||
|
#### Pre-setup the MFA/TOTP method
|
||||||
|
* method: `GET`
|
||||||
|
* Authentication: required
|
||||||
|
* OAuth scope: `write:security`
|
||||||
|
* Response: JSON. Returns `{"key": [secret_key], "provisioning_uri": "[qr code uri]" }` when successful, otherwise returns HTTP 422 `{"error": "error_msg"}`
|
||||||
|
|
||||||
|
## `/api/pleroma/accounts/mfa/confirm/totp`
|
||||||
|
#### Confirms & enables MFA/TOTP support for user account.
|
||||||
|
* method: `POST`
|
||||||
|
* Authentication: required
|
||||||
|
* OAuth scope: `write:security`
|
||||||
|
* Params:
|
||||||
|
* `password`: user's password
|
||||||
|
* `code`: token from TOTP App
|
||||||
|
* Response: JSON. Returns `{}` if the enable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
|
||||||
|
|
||||||
|
|
||||||
|
## `/api/pleroma/accounts/mfa/totp`
|
||||||
|
#### Disables MFA/TOTP method for user account.
|
||||||
|
* method: `DELETE`
|
||||||
|
* Authentication: required
|
||||||
|
* OAuth scope: `write:security`
|
||||||
|
* Params:
|
||||||
|
* `password`: user's password
|
||||||
|
* Response: JSON. Returns `{}` if the disable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
|
||||||
|
* Example response: `{"error": "Invalid password."}`
|
||||||
|
|
||||||
|
## `/api/pleroma/accounts/mfa/backup_codes`
|
||||||
|
#### Generstes backup codes MFA for user account.
|
||||||
|
* method: `GET`
|
||||||
|
* Authentication: required
|
||||||
|
* OAuth scope: `write:security`
|
||||||
|
* Response: JSON. Returns `{"codes": codes}`when successful, otherwise HTTP 422 `{"error": "[error message]"}`
|
||||||
|
|
||||||
|
## `/api/pleroma/admin/`
|
||||||
See [Admin-API](admin_api.md)
|
See [Admin-API](admin_api.md)
|
||||||
|
|
||||||
## `/api/v1/pleroma/notifications/read`
|
## `/api/v1/pleroma/notifications/read`
|
||||||
|
|
|
@ -49,11 +49,11 @@ Feel free to contact us to be added to this list!
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: Streaming Ready
|
- Features: Streaming Ready
|
||||||
|
|
||||||
### Roma
|
### Fedi
|
||||||
- Homepage: <https://www.pleroma.com/#mobileApps>
|
- Homepage: <https://www.fediapp.com/>
|
||||||
- Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android)
|
- Source Code: Proprietary, but free
|
||||||
- Platforms: iOS, Android
|
- Platforms: iOS, Android
|
||||||
- Features: No Streaming
|
- Features: Pleroma-specific features like Reactions
|
||||||
|
|
||||||
### Tusky
|
### Tusky
|
||||||
- Homepage: <https://tuskyapp.github.io/>
|
- Homepage: <https://tuskyapp.github.io/>
|
||||||
|
|
|
@ -8,6 +8,10 @@ For from source installations Pleroma configuration works by first importing the
|
||||||
|
|
||||||
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
|
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
|
||||||
|
|
||||||
|
## :chat
|
||||||
|
|
||||||
|
* `enabled` - Enables the backend chat. Defaults to `true`.
|
||||||
|
|
||||||
## :instance
|
## :instance
|
||||||
* `name`: The instance’s name.
|
* `name`: The instance’s name.
|
||||||
* `email`: Email used to reach an Administrator/Moderator of the instance.
|
* `email`: Email used to reach an Administrator/Moderator of the instance.
|
||||||
|
@ -903,12 +907,18 @@ config :auto_linker,
|
||||||
|
|
||||||
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
|
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
|
||||||
|
|
||||||
|
|
||||||
## :configurable_from_database
|
## :configurable_from_database
|
||||||
|
|
||||||
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
|
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
|
||||||
|
|
||||||
|
|
||||||
|
### Multi-factor authentication - :two_factor_authentication
|
||||||
|
* `totp` - a list containing TOTP configuration
|
||||||
|
- `digits` - Determines the length of a one-time pass-code in characters. Defaults to 6 characters.
|
||||||
|
- `period` - a period for which the TOTP code will be valid in seconds. Defaults to 30 seconds.
|
||||||
|
* `backup_codes` - a list containing backup codes configuration
|
||||||
|
- `number` - number of backup codes to generate.
|
||||||
|
- `length` - backup code length. Defaults to 16 characters.
|
||||||
|
|
||||||
## Restrict entities access for unauthenticated users
|
## Restrict entities access for unauthenticated users
|
||||||
|
|
||||||
|
@ -925,3 +935,8 @@ Restrict access for unauthenticated users to timelines (public and federate), us
|
||||||
* `activities` - statuses
|
* `activities` - statuses
|
||||||
* `local`
|
* `local`
|
||||||
* `remote`
|
* `remote`
|
||||||
|
|
||||||
|
|
||||||
|
## Pleroma.Web.ApiSpec.CastAndValidate
|
||||||
|
|
||||||
|
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
|
||||||
|
|
|
@ -32,9 +32,8 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||||
|
|
||||||
<VirtualHost *:443>
|
<VirtualHost *:443>
|
||||||
SSLEngine on
|
SSLEngine on
|
||||||
SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem
|
SSLCertificateFile /etc/letsencrypt/live/${servername}/fullchain.pem
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
|
SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
|
||||||
SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem
|
|
||||||
|
|
||||||
# Mozilla modern configuration, tweak to your needs
|
# Mozilla modern configuration, tweak to your needs
|
||||||
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
|
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.UserInviteToken
|
alias Pleroma.UserInviteToken
|
||||||
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
|
|
||||||
@shortdoc "Manages Pleroma users"
|
@shortdoc "Manages Pleroma users"
|
||||||
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
|
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
|
||||||
|
@ -96,8 +98,9 @@ def run(["new", nickname, email | rest]) do
|
||||||
def run(["rm", nickname]) do
|
def run(["rm", nickname]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
||||||
User.perform(:delete, user)
|
{:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
|
||||||
|
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
||||||
shell_info("User #{nickname} deleted.")
|
shell_info("User #{nickname} deleted.")
|
||||||
else
|
else
|
||||||
_ -> shell_error("No local user #{nickname}")
|
_ -> shell_error("No local user #{nickname}")
|
||||||
|
|
|
@ -173,7 +173,14 @@ defp chat_enabled?, do: Config.get([:chat, :enabled])
|
||||||
defp streamer_child(env) when env in [:test, :benchmark], do: []
|
defp streamer_child(env) when env in [:test, :benchmark], do: []
|
||||||
|
|
||||||
defp streamer_child(_) do
|
defp streamer_child(_) do
|
||||||
[Pleroma.Web.Streamer.supervisor()]
|
[
|
||||||
|
{Registry,
|
||||||
|
[
|
||||||
|
name: Pleroma.Web.Streamer.registry(),
|
||||||
|
keys: :duplicate,
|
||||||
|
partitions: System.schedulers_online()
|
||||||
|
]}
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp chat_child(_env, true) do
|
defp chat_child(_env, true) do
|
||||||
|
|
|
@ -20,4 +20,9 @@ defmodule Pleroma.Constants do
|
||||||
"deleted_activity_id"
|
"deleted_activity_id"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const(static_only_files,
|
||||||
|
do:
|
||||||
|
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -128,7 +128,7 @@ def for_user(user, params \\ %{}) do
|
||||||
|> Pleroma.Pagination.fetch_paginated(params)
|
|> Pleroma.Pagination.fetch_paginated(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_recipients(query, user, %{"recipients" => user_ids}) do
|
def restrict_recipients(query, user, %{recipients: user_ids}) do
|
||||||
user_binary_ids =
|
user_binary_ids =
|
||||||
[user.id | user_ids]
|
[user.id | user_ids]
|
||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|
@ -172,7 +172,7 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
|
||||||
| last_activity_id: activity_id
|
| last_activity_id: activity_id
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|> Enum.filter(& &1.last_activity_id)
|
|> Enum.reject(&is_nil(&1.last_activity_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(_, _ \\ [])
|
def get(_, _ \\ [])
|
||||||
|
|
|
@ -89,11 +89,10 @@ def delete(%Pleroma.Filter{id: filter_key} = filter) when is_nil(filter_key) do
|
||||||
|> Repo.delete()
|
|> Repo.delete()
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(%Pleroma.Filter{} = filter) do
|
def update(%Pleroma.Filter{} = filter, params) do
|
||||||
destination = Map.from_struct(filter)
|
filter
|
||||||
|
|> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
|
||||||
Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
|
|> validate_required([:phrase, :context])
|
||||||
|> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
|
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias __MODULE__
|
||||||
|
|
||||||
@timelines ["notifications"]
|
@timelines ["notifications"]
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
schema "markers" do
|
schema "markers" do
|
||||||
field(:last_read_id, :string, default: "")
|
field(:last_read_id, :string, default: "")
|
||||||
field(:timeline, :string, default: "")
|
field(:timeline, :string, default: "")
|
||||||
field(:lock_version, :integer, default: 0)
|
field(:lock_version, :integer, default: 0)
|
||||||
|
field(:unread_count, :integer, default: 0, virtual: true)
|
||||||
|
|
||||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Gets markers by user and timeline."
|
||||||
|
@spec get_markers(User.t(), list(String)) :: list(t())
|
||||||
def get_markers(user, timelines \\ []) do
|
def get_markers(user, timelines \\ []) do
|
||||||
Repo.all(get_query(user, timelines))
|
user
|
||||||
|
|> get_query(timelines)
|
||||||
|
|> unread_count_query()
|
||||||
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec upsert(User.t(), map()) :: {:ok | :error, any()}
|
||||||
def upsert(%User{} = user, attrs) do
|
def upsert(%User{} = user, attrs) do
|
||||||
attrs
|
attrs
|
||||||
|> Map.take(@timelines)
|
|> Map.take(@timelines)
|
||||||
|
@ -45,6 +55,27 @@ def upsert(%User{} = user, attrs) do
|
||||||
|> Repo.transaction()
|
|> Repo.transaction()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
|
||||||
|
def multi_set_last_read_id(multi, %User{} = user, "notifications") do
|
||||||
|
multi
|
||||||
|
|> Multi.run(:counters, fn _repo, _changes ->
|
||||||
|
{:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
|
||||||
|
end)
|
||||||
|
|> Multi.insert(
|
||||||
|
:marker,
|
||||||
|
fn %{counters: attrs} ->
|
||||||
|
%Marker{timeline: "notifications", user_id: user.id}
|
||||||
|
|> struct(attrs)
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
end,
|
||||||
|
returning: true,
|
||||||
|
on_conflict: {:replace, [:last_read_id]},
|
||||||
|
conflict_target: [:user_id, :timeline]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def multi_set_last_read_id(multi, _, _), do: multi
|
||||||
|
|
||||||
defp get_marker(user, timeline) do
|
defp get_marker(user, timeline) do
|
||||||
case Repo.find_resource(get_query(user, timeline)) do
|
case Repo.find_resource(get_query(user, timeline)) do
|
||||||
{:ok, marker} -> %__MODULE__{marker | user: user}
|
{:ok, marker} -> %__MODULE__{marker | user: user}
|
||||||
|
@ -71,4 +102,16 @@ defp get_query(user, timelines) do
|
||||||
|> by_user_id(user.id)
|
|> by_user_id(user.id)
|
||||||
|> by_timeline(timelines)
|
|> by_timeline(timelines)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp unread_count_query(query) do
|
||||||
|
from(
|
||||||
|
q in query,
|
||||||
|
left_join: n in "notifications",
|
||||||
|
on: n.user_id == q.user_id and n.seen == false,
|
||||||
|
group_by: [:id],
|
||||||
|
select_merge: %{
|
||||||
|
unread_count: fragment("count(?)", n.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA do
|
||||||
|
@moduledoc """
|
||||||
|
The MFA context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Comeonin.Pbkdf2
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
alias Pleroma.MFA.BackupCodes
|
||||||
|
alias Pleroma.MFA.Changeset
|
||||||
|
alias Pleroma.MFA.Settings
|
||||||
|
alias Pleroma.MFA.TOTP
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns MFA methods the user has enabled.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Pleroma.MFA.supported_method(User)
|
||||||
|
"totp, u2f"
|
||||||
|
"""
|
||||||
|
@spec supported_methods(User.t()) :: String.t()
|
||||||
|
def supported_methods(user) do
|
||||||
|
settings = fetch_settings(user)
|
||||||
|
|
||||||
|
Settings.mfa_methods()
|
||||||
|
|> Enum.reduce([], fn m, acc ->
|
||||||
|
if method_enabled?(m, settings) do
|
||||||
|
acc ++ [m]
|
||||||
|
else
|
||||||
|
acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.join(",")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Checks that user enabled MFA"
|
||||||
|
def require?(user) do
|
||||||
|
fetch_settings(user).enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Display MFA settings of user
|
||||||
|
"""
|
||||||
|
def mfa_settings(user) do
|
||||||
|
settings = fetch_settings(user)
|
||||||
|
|
||||||
|
Settings.mfa_methods()
|
||||||
|
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|
||||||
|
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def fetch_settings(%User{} = user) do
|
||||||
|
user.multi_factor_authentication_settings || %Settings{}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "clears backup codes"
|
||||||
|
def invalidate_backup_code(%User{} = user, hash_code) do
|
||||||
|
%{backup_codes: codes} = fetch_settings(user)
|
||||||
|
|
||||||
|
user
|
||||||
|
|> Changeset.cast_backup_codes(codes -- [hash_code])
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "generates backup codes"
|
||||||
|
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
|
||||||
|
def generate_backup_codes(%User{} = user) do
|
||||||
|
with codes <- BackupCodes.generate(),
|
||||||
|
hashed_codes <- Enum.map(codes, &Pbkdf2.hashpwsalt/1),
|
||||||
|
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
|
||||||
|
{:ok, _} <- User.update_and_set_cache(changeset) do
|
||||||
|
{:ok, codes}
|
||||||
|
else
|
||||||
|
{:error, msg} ->
|
||||||
|
%{error: msg}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates secret key and set delivery_type to 'app' for TOTP method.
|
||||||
|
"""
|
||||||
|
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def setup_totp(user) do
|
||||||
|
user
|
||||||
|
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Confirms the TOTP method for user.
|
||||||
|
|
||||||
|
`attrs`:
|
||||||
|
`password` - current user password
|
||||||
|
`code` - TOTP token
|
||||||
|
"""
|
||||||
|
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||||
|
def confirm_totp(%User{} = user, attrs) do
|
||||||
|
with settings <- user.multi_factor_authentication_settings.totp,
|
||||||
|
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
|
||||||
|
user
|
||||||
|
|> Changeset.confirm_totp()
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Disables the TOTP method for user.
|
||||||
|
|
||||||
|
`attrs`:
|
||||||
|
`password` - current user password
|
||||||
|
"""
|
||||||
|
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def disable_totp(%User{} = user) do
|
||||||
|
user
|
||||||
|
|> Changeset.disable_totp()
|
||||||
|
|> Changeset.disable()
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Force disables all MFA methods for user.
|
||||||
|
"""
|
||||||
|
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
|
def disable(%User{} = user) do
|
||||||
|
user
|
||||||
|
|> Changeset.disable_totp()
|
||||||
|
|> Changeset.disable(true)
|
||||||
|
|> User.update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the user has MFA method enabled.
|
||||||
|
"""
|
||||||
|
def method_enabled?(method, settings) do
|
||||||
|
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
|
||||||
|
true
|
||||||
|
else
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the user has enabled at least one MFA method.
|
||||||
|
"""
|
||||||
|
def enabled?(settings) do
|
||||||
|
Settings.mfa_methods()
|
||||||
|
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|
||||||
|
|> Enum.any?()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA.BackupCodes do
|
||||||
|
@moduledoc """
|
||||||
|
This module contains functions for generating backup codes.
|
||||||
|
"""
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
@config_ns [:instance, :multi_factor_authentication, :backup_codes]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates backup codes.
|
||||||
|
"""
|
||||||
|
@spec generate(Keyword.t()) :: list(String.t())
|
||||||
|
def generate(opts \\ []) do
|
||||||
|
number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
|
||||||
|
code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
|
||||||
|
|
||||||
|
Enum.map(1..number_of_codes, fn _ ->
|
||||||
|
:crypto.strong_rand_bytes(div(code_length, 2))
|
||||||
|
|> Base.encode16(case: :lower)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
|
||||||
|
|
||||||
|
defp default_backup_codes_code_length,
|
||||||
|
do: Config.get(@config_ns ++ [:length], 16)
|
||||||
|
end
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA.Changeset do
|
||||||
|
alias Pleroma.MFA
|
||||||
|
alias Pleroma.MFA.Settings
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
|
||||||
|
settings =
|
||||||
|
changeset
|
||||||
|
|> Ecto.Changeset.apply_changes()
|
||||||
|
|> MFA.fetch_settings()
|
||||||
|
|
||||||
|
if force || not MFA.enabled?(settings) do
|
||||||
|
put_change(changeset, %Settings{settings | enabled: false})
|
||||||
|
else
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||||
|
user
|
||||||
|
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
|
||||||
|
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
|
||||||
|
|
||||||
|
user
|
||||||
|
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_totp(%User{} = user, attrs) do
|
||||||
|
mfa_settings = MFA.fetch_settings(user)
|
||||||
|
|
||||||
|
totp_settings =
|
||||||
|
%Settings.TOTP{}
|
||||||
|
|> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
|
||||||
|
|
||||||
|
user
|
||||||
|
|> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_backup_codes(%User{} = user, codes) do
|
||||||
|
user
|
||||||
|
|> put_change(%Settings{
|
||||||
|
user.multi_factor_authentication_settings
|
||||||
|
| backup_codes: codes
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_change(%User{} = user, settings) do
|
||||||
|
user
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
|> put_change(settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_change(%Ecto.Changeset{} = changeset, settings) do
|
||||||
|
changeset
|
||||||
|
|> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA.Settings do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
|
||||||
|
@mfa_methods [:totp]
|
||||||
|
embedded_schema do
|
||||||
|
field(:enabled, :boolean, default: false)
|
||||||
|
field(:backup_codes, {:array, :string}, default: [])
|
||||||
|
|
||||||
|
embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
|
||||||
|
field(:secret, :string)
|
||||||
|
# app | sms
|
||||||
|
field(:delivery_type, :string, default: "app")
|
||||||
|
field(:confirmed, :boolean, default: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def mfa_methods, do: @mfa_methods
|
||||||
|
end
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA.Token do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Query
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.OAuth.Authorization
|
||||||
|
alias Pleroma.Web.OAuth.Token, as: OAuthToken
|
||||||
|
|
||||||
|
@expires 300
|
||||||
|
|
||||||
|
schema "mfa_tokens" do
|
||||||
|
field(:token, :string)
|
||||||
|
field(:valid_until, :naive_datetime_usec)
|
||||||
|
|
||||||
|
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||||
|
belongs_to(:authorization, Authorization)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_token(token) do
|
||||||
|
from(
|
||||||
|
t in __MODULE__,
|
||||||
|
where: t.token == ^token,
|
||||||
|
preload: [:user, :authorization]
|
||||||
|
)
|
||||||
|
|> Repo.find_resource()
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(token) do
|
||||||
|
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
|
||||||
|
{:expired, false} <- {:expired, is_expired?(token)} do
|
||||||
|
{:ok, token}
|
||||||
|
else
|
||||||
|
{:expired, _} -> {:error, :expired_token}
|
||||||
|
{:fetch_token, _} -> {:error, :not_found}
|
||||||
|
error -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_token(%User{} = user) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> change
|
||||||
|
|> assign_user(user)
|
||||||
|
|> put_token
|
||||||
|
|> put_valid_until
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_token(user, authorization) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> change
|
||||||
|
|> assign_user(user)
|
||||||
|
|> assign_authorization(authorization)
|
||||||
|
|> put_token
|
||||||
|
|> put_valid_until
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_user(changeset, user) do
|
||||||
|
changeset
|
||||||
|
|> put_assoc(:user, user)
|
||||||
|
|> validate_required([:user])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_authorization(changeset, authorization) do
|
||||||
|
changeset
|
||||||
|
|> put_assoc(:authorization, authorization)
|
||||||
|
|> validate_required([:authorization])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_token(changeset) do
|
||||||
|
changeset
|
||||||
|
|> change(%{token: OAuthToken.Utils.generate_token()})
|
||||||
|
|> validate_required([:token])
|
||||||
|
|> unique_constraint(:token)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_valid_until(changeset) do
|
||||||
|
expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
|
||||||
|
|
||||||
|
changeset
|
||||||
|
|> change(%{valid_until: expires_in})
|
||||||
|
|> validate_required([:valid_until])
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_expired?(%__MODULE__{valid_until: valid_until}) do
|
||||||
|
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_expired?(_), do: false
|
||||||
|
|
||||||
|
def delete_expired_tokens do
|
||||||
|
from(
|
||||||
|
q in __MODULE__,
|
||||||
|
where: fragment("?", q.valid_until) < ^Timex.now()
|
||||||
|
)
|
||||||
|
|> Repo.delete_all()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.MFA.TOTP do
|
||||||
|
@moduledoc """
|
||||||
|
This module represents functions to create secrets for
|
||||||
|
TOTP Application as well as validate them with a time based token.
|
||||||
|
"""
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
@config_ns [:instance, :multi_factor_authentication, :totp]
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||||
|
"""
|
||||||
|
def provisioning_uri(secret, label, opts \\ []) do
|
||||||
|
query =
|
||||||
|
%{
|
||||||
|
secret: secret,
|
||||||
|
issuer: Keyword.get(opts, :issuer, default_issuer()),
|
||||||
|
digits: Keyword.get(opts, :digits, default_digits()),
|
||||||
|
period: Keyword.get(opts, :period, default_period())
|
||||||
|
}
|
||||||
|
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|> URI.encode_query()
|
||||||
|
|
||||||
|
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|
||||||
|
|> URI.to_string()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp default_period, do: Config.get(@config_ns ++ [:period])
|
||||||
|
defp default_digits, do: Config.get(@config_ns ++ [:digits])
|
||||||
|
|
||||||
|
defp default_issuer,
|
||||||
|
do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
|
||||||
|
|
||||||
|
@doc "Creates a random Base 32 encoded string"
|
||||||
|
def generate_secret do
|
||||||
|
Base.encode32(:crypto.strong_rand_bytes(10))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Generates a valid token based on a secret"
|
||||||
|
def generate_token(secret) do
|
||||||
|
:pot.totp(secret)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validates a given token based on a secret.
|
||||||
|
|
||||||
|
optional parameters:
|
||||||
|
`token_length` default `6`
|
||||||
|
`interval_length` default `30`
|
||||||
|
`window` default 0
|
||||||
|
|
||||||
|
Returns {:ok, :pass} if the token is valid and
|
||||||
|
{:error, :invalid_token} if it is not.
|
||||||
|
"""
|
||||||
|
@spec validate_token(String.t(), String.t()) ::
|
||||||
|
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||||
|
def validate_token(secret, token)
|
||||||
|
when is_binary(secret) and is_binary(token) do
|
||||||
|
opts = [
|
||||||
|
token_length: default_digits(),
|
||||||
|
interval_length: default_period()
|
||||||
|
]
|
||||||
|
|
||||||
|
validate_token(secret, token, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_token(_, _), do: {:error, :invalid_secret_and_token}
|
||||||
|
|
||||||
|
@doc "See `validate_token/2`"
|
||||||
|
@spec validate_token(String.t(), String.t(), Keyword.t()) ::
|
||||||
|
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||||
|
def validate_token(secret, token, options)
|
||||||
|
when is_binary(secret) and is_binary(token) do
|
||||||
|
case :pot.valid_totp(token, secret, options) do
|
||||||
|
true -> {:ok, :pass}
|
||||||
|
false -> {:error, :invalid_token}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
|
||||||
|
end
|
|
@ -5,8 +5,10 @@
|
||||||
defmodule Pleroma.Notification do
|
defmodule Pleroma.Notification do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Ecto.Multi
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.FollowingRelationship
|
alias Pleroma.FollowingRelationship
|
||||||
|
alias Pleroma.Marker
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
|
@ -34,11 +36,30 @@ defmodule Pleroma.Notification do
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec unread_notifications_count(User.t()) :: integer()
|
||||||
|
def unread_notifications_count(%User{id: user_id}) do
|
||||||
|
from(q in __MODULE__,
|
||||||
|
where: q.user_id == ^user_id and q.seen == false
|
||||||
|
)
|
||||||
|
|> Repo.aggregate(:count, :id)
|
||||||
|
end
|
||||||
|
|
||||||
def changeset(%Notification{} = notification, attrs) do
|
def changeset(%Notification{} = notification, attrs) do
|
||||||
notification
|
notification
|
||||||
|> cast(attrs, [:seen])
|
|> cast(attrs, [:seen])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
|
||||||
|
def last_read_query(user) do
|
||||||
|
from(q in Pleroma.Notification,
|
||||||
|
where: q.user_id == ^user.id,
|
||||||
|
where: q.seen == true,
|
||||||
|
select: type(q.id, :string),
|
||||||
|
limit: 1,
|
||||||
|
order_by: [desc: :id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp for_user_query_ap_id_opts(user, opts) do
|
defp for_user_query_ap_id_opts(user, opts) do
|
||||||
ap_id_relationships =
|
ap_id_relationships =
|
||||||
[:block] ++
|
[:block] ++
|
||||||
|
@ -185,25 +206,23 @@ def for_user_since(user, date) do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_read_up_to(%{id: user_id} = _user, id) do
|
def set_read_up_to(%{id: user_id} = user, id) do
|
||||||
query =
|
query =
|
||||||
from(
|
from(
|
||||||
n in Notification,
|
n in Notification,
|
||||||
where: n.user_id == ^user_id,
|
where: n.user_id == ^user_id,
|
||||||
where: n.id <= ^id,
|
where: n.id <= ^id,
|
||||||
where: n.seen == false,
|
where: n.seen == false,
|
||||||
update: [
|
|
||||||
set: [
|
|
||||||
seen: true,
|
|
||||||
updated_at: ^NaiveDateTime.utc_now()
|
|
||||||
]
|
|
||||||
],
|
|
||||||
# Ideally we would preload object and activities here
|
# Ideally we would preload object and activities here
|
||||||
# but Ecto does not support preloads in update_all
|
# but Ecto does not support preloads in update_all
|
||||||
select: n.id
|
select: n.id
|
||||||
)
|
)
|
||||||
|
|
||||||
{_, notification_ids} = Repo.update_all(query, [])
|
{:ok, %{ids: {_, notification_ids}}} =
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|
||||||
|
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||||
|
|> Repo.transaction()
|
||||||
|
|
||||||
Notification
|
Notification
|
||||||
|> where([n], n.id in ^notification_ids)
|
|> where([n], n.id in ^notification_ids)
|
||||||
|
@ -220,11 +239,18 @@ def set_read_up_to(%{id: user_id} = _user, id) do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec read_one(User.t(), String.t()) ::
|
||||||
|
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
|
||||||
def read_one(%User{} = user, notification_id) do
|
def read_one(%User{} = user, notification_id) do
|
||||||
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
|
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
|
||||||
notification
|
Multi.new()
|
||||||
|> changeset(%{seen: true})
|
|> Multi.update(:update, changeset(notification, %{seen: true}))
|
||||||
|> Repo.update()
|
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> case do
|
||||||
|
{:ok, %{update: notification}} -> {:ok, notification}
|
||||||
|
{:error, :update, changeset, _} -> {:error, changeset}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -293,17 +319,8 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
|
|
||||||
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
|
|
||||||
Activity.follow_accepted?(activity) do
|
|
||||||
do_create_notifications(activity)
|
|
||||||
else
|
|
||||||
{:ok, []}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||||
when type in ["Like", "Announce", "Move", "EmojiReact"] do
|
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
|
||||||
do_create_notifications(activity)
|
do_create_notifications(activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -325,8 +342,11 @@ defp do_create_notifications(%Activity{} = activity) do
|
||||||
# TODO move to sql, too.
|
# TODO move to sql, too.
|
||||||
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
|
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
|
||||||
unless skip?(activity, user) do
|
unless skip?(activity, user) do
|
||||||
notification = %Notification{user_id: user.id, activity: activity}
|
{:ok, %{notification: notification}} =
|
||||||
{:ok, notification} = Repo.insert(notification)
|
Multi.new()
|
||||||
|
|> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
|
||||||
|
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||||
|
|> Repo.transaction()
|
||||||
|
|
||||||
if do_send do
|
if do_send do
|
||||||
Streamer.stream(["user", "user:notification"], notification)
|
Streamer.stream(["user", "user:notification"], notification)
|
||||||
|
@ -348,13 +368,7 @@ def get_notified_from_activity(activity, local_only \\ true)
|
||||||
|
|
||||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||||
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
|
||||||
potential_receiver_ap_ids =
|
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
|
||||||
[]
|
|
||||||
|> Utils.maybe_notify_to_recipients(activity)
|
|
||||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
|
||||||
|> Utils.maybe_notify_subscribers(activity)
|
|
||||||
|> Utils.maybe_notify_followers(activity)
|
|
||||||
|> Enum.uniq()
|
|
||||||
|
|
||||||
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
|
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
|
||||||
|
|
||||||
|
@ -372,6 +386,27 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo
|
||||||
|
|
||||||
def get_notified_from_activity(_, _local_only), do: {[], []}
|
def get_notified_from_activity(_, _local_only), do: {[], []}
|
||||||
|
|
||||||
|
# For some activities, only notify the author of the object
|
||||||
|
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
|
||||||
|
when type in ~w{Like Announce EmojiReact} do
|
||||||
|
case Object.get_cached_by_ap_id(object_id) do
|
||||||
|
%Object{data: %{"actor" => actor}} ->
|
||||||
|
[actor]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_potential_receiver_ap_ids(activity) 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()
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
|
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
|
||||||
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
|
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
|
||||||
|
|
||||||
|
|
|
@ -15,26 +15,25 @@ def init(options) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def perform(
|
||||||
|
%{
|
||||||
|
assigns: %{
|
||||||
|
auth_credentials: %{password: _},
|
||||||
|
user: %User{multi_factor_authentication_settings: %{enabled: true}}
|
||||||
|
}
|
||||||
|
} = conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
conn
|
||||||
|
|> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
|
||||||
def perform(%{assigns: %{user: %User{}}} = conn, _) do
|
def perform(%{assigns: %{user: %User{}}} = conn, _) do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(conn, options) do
|
def perform(conn, _) do
|
||||||
perform =
|
|
||||||
cond do
|
|
||||||
options[:if_func] -> options[:if_func].()
|
|
||||||
options[:unless_func] -> !options[:unless_func].()
|
|
||||||
true -> true
|
|
||||||
end
|
|
||||||
|
|
||||||
if perform do
|
|
||||||
fail(conn)
|
|
||||||
else
|
|
||||||
conn
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fail(conn) do
|
|
||||||
conn
|
conn
|
||||||
|> render_error(:forbidden, "Invalid credentials.")
|
|> render_error(:forbidden, "Invalid credentials.")
|
||||||
|> halt()
|
|> halt()
|
||||||
|
|
|
@ -19,6 +19,9 @@ def call(conn, _opts) do
|
||||||
|
|
||||||
def federating?, do: Pleroma.Config.get([:instance, :federating])
|
def federating?, do: Pleroma.Config.get([:instance, :federating])
|
||||||
|
|
||||||
|
# Definition for the use in :if_func / :unless_func plug options
|
||||||
|
def federating?(_conn), do: federating?()
|
||||||
|
|
||||||
defp fail(conn) do
|
defp fail(conn) do
|
||||||
conn
|
conn
|
||||||
|> put_status(404)
|
|> put_status(404)
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Plugs.InstanceStatic do
|
defmodule Pleroma.Plugs.InstanceStatic do
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
||||||
|
|
||||||
|
@ -21,9 +23,6 @@ def file_path(path) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
|
|
||||||
sw-pleroma.js)
|
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
opts
|
opts
|
||||||
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
||||||
|
@ -31,7 +30,7 @@ def init(opts) do
|
||||||
|> Plug.Static.init()
|
|> Plug.Static.init()
|
||||||
end
|
end
|
||||||
|
|
||||||
for only <- @only do
|
for only <- Pleroma.Constants.static_only_files() do
|
||||||
at = Plug.Router.Utils.split("/")
|
at = Plug.Router.Utils.split("/")
|
||||||
|
|
||||||
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
||||||
|
|
|
@ -13,8 +13,9 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||||
def init(options), do: options
|
def init(options), do: options
|
||||||
|
|
||||||
defp key_id_from_conn(conn) do
|
defp key_id_from_conn(conn) do
|
||||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn) do
|
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
Signature.key_id_to_actor_id(key_id)
|
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||||
|
ap_id
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Signature do
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
|
||||||
def key_id_to_actor_id(key_id) do
|
def key_id_to_actor_id(key_id) do
|
||||||
uri =
|
uri =
|
||||||
|
@ -21,12 +22,23 @@ def key_id_to_actor_id(key_id) do
|
||||||
uri
|
uri
|
||||||
end
|
end
|
||||||
|
|
||||||
URI.to_string(uri)
|
maybe_ap_id = URI.to_string(uri)
|
||||||
|
|
||||||
|
case Types.ObjectID.cast(maybe_ap_id) do
|
||||||
|
{:ok, ap_id} ->
|
||||||
|
{:ok, ap_id}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
|
||||||
|
%{"ap_id" => ap_id} -> {:ok, ap_id}
|
||||||
|
_ -> {:error, maybe_ap_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_public_key(conn) do
|
def fetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
actor_id <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
else
|
else
|
||||||
|
@ -37,7 +49,7 @@ def fetch_public_key(conn) do
|
||||||
|
|
||||||
def refetch_public_key(conn) do
|
def refetch_public_key(conn) do
|
||||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||||
actor_id <- key_id_to_actor_id(kid),
|
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||||
{:ok, public_key}
|
{:ok, public_key}
|
||||||
|
|
|
@ -91,7 +91,7 @@ def calculate_stat_data do
|
||||||
peers: peers,
|
peers: peers,
|
||||||
stats: %{
|
stats: %{
|
||||||
domain_count: domain_count,
|
domain_count: domain_count,
|
||||||
status_count: status_count,
|
status_count: status_count || 0,
|
||||||
user_count: user_count
|
user_count: user_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.Formatter
|
alias Pleroma.Formatter
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
|
alias Pleroma.MFA
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Registration
|
alias Pleroma.Registration
|
||||||
|
@ -29,7 +30,9 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.UserRelationship
|
alias Pleroma.UserRelationship
|
||||||
alias Pleroma.Web
|
alias Pleroma.Web
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||||
|
@ -113,7 +116,6 @@ defmodule Pleroma.User do
|
||||||
field(:is_admin, :boolean, default: false)
|
field(:is_admin, :boolean, default: false)
|
||||||
field(:show_role, :boolean, default: true)
|
field(:show_role, :boolean, default: true)
|
||||||
field(:settings, :map, default: nil)
|
field(:settings, :map, default: nil)
|
||||||
field(:magic_key, :string, default: nil)
|
|
||||||
field(:uri, Types.Uri, default: nil)
|
field(:uri, Types.Uri, default: nil)
|
||||||
field(:hide_followers_count, :boolean, default: false)
|
field(:hide_followers_count, :boolean, default: false)
|
||||||
field(:hide_follows_count, :boolean, default: false)
|
field(:hide_follows_count, :boolean, default: false)
|
||||||
|
@ -189,6 +191,12 @@ defmodule Pleroma.User do
|
||||||
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
|
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
|
||||||
field(:subscribers, {:array, :string}, default: [])
|
field(:subscribers, {:array, :string}, default: [])
|
||||||
|
|
||||||
|
embeds_one(
|
||||||
|
:multi_factor_authentication_settings,
|
||||||
|
MFA.Settings,
|
||||||
|
on_replace: :delete
|
||||||
|
)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -387,7 +395,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||||
:banner,
|
:banner,
|
||||||
:locked,
|
:locked,
|
||||||
:last_refreshed_at,
|
:last_refreshed_at,
|
||||||
:magic_key,
|
|
||||||
:uri,
|
:uri,
|
||||||
:follower_address,
|
:follower_address,
|
||||||
:following_address,
|
:following_address,
|
||||||
|
@ -927,6 +934,7 @@ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_by_nickname(String.t()) :: User.t() | nil
|
||||||
def get_by_nickname(nickname) do
|
def get_by_nickname(nickname) do
|
||||||
Repo.get_by(User, nickname: nickname) ||
|
Repo.get_by(User, nickname: nickname) ||
|
||||||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
|
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
|
||||||
|
@ -1427,8 +1435,6 @@ def perform(:force_password_reset, user), do: force_password_reset(user)
|
||||||
|
|
||||||
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
||||||
def perform(:delete, %User{} = user) do
|
def perform(:delete, %User{} = user) do
|
||||||
{:ok, _user} = ActivityPub.delete(user)
|
|
||||||
|
|
||||||
# Remove all relationships
|
# Remove all relationships
|
||||||
user
|
user
|
||||||
|> get_followers()
|
|> get_followers()
|
||||||
|
@ -1445,9 +1451,16 @@ def perform(:delete, %User{} = user) do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
delete_user_activities(user)
|
delete_user_activities(user)
|
||||||
|
|
||||||
|
if user.local do
|
||||||
|
user
|
||||||
|
|> change(%{deactivated: true, email: nil})
|
||||||
|
|> update_and_set_cache()
|
||||||
|
else
|
||||||
invalidate_cache(user)
|
invalidate_cache(user)
|
||||||
Repo.delete(user)
|
Repo.delete(user)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
||||||
|
|
||||||
|
@ -1531,37 +1544,29 @@ def follow_import(%User{} = follower, followed_identifiers)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_user_activities(%User{ap_id: ap_id}) do
|
def delete_user_activities(%User{ap_id: ap_id} = user) do
|
||||||
ap_id
|
ap_id
|
||||||
|> Activity.Queries.by_actor()
|
|> Activity.Queries.by_actor()
|
||||||
|> RepoStreamer.chunk_stream(50)
|
|> RepoStreamer.chunk_stream(50)
|
||||||
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|
|> Stream.each(fn activities ->
|
||||||
|
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
|
||||||
|
end)
|
||||||
|> Stream.run()
|
|> Stream.run()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
|
defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
|
||||||
activity
|
{:ok, delete_data, _} = Builder.delete(user, object)
|
||||||
|> Object.normalize()
|
|
||||||
|> ActivityPub.delete()
|
Pipeline.common_pipeline(delete_data, local: user.local)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
|
defp delete_activity(%{data: %{"type" => type}} = activity, user)
|
||||||
object = Object.normalize(activity)
|
when type in ["Like", "Announce"] do
|
||||||
|
{:ok, undo, _} = Builder.undo(user, activity)
|
||||||
activity.actor
|
Pipeline.common_pipeline(undo, local: user.local)
|
||||||
|> get_cached_by_ap_id()
|
|
||||||
|> ActivityPub.unlike(object)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
|
defp delete_activity(_activity, _user), do: "Doing nothing"
|
||||||
object = Object.normalize(activity)
|
|
||||||
|
|
||||||
activity.actor
|
|
||||||
|> get_cached_by_ap_id()
|
|
||||||
|> ActivityPub.unannounce(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp delete_activity(_activity), do: "Doing nothing"
|
|
||||||
|
|
||||||
def html_filter_policy(%User{no_rich_text: true}) do
|
def html_filter_policy(%User{no_rich_text: true}) do
|
||||||
Pleroma.HTML.Scrubber.TwitterText
|
Pleroma.HTML.Scrubber.TwitterText
|
||||||
|
|
|
@ -45,6 +45,7 @@ defmodule Pleroma.User.Query do
|
||||||
is_admin: boolean(),
|
is_admin: boolean(),
|
||||||
is_moderator: boolean(),
|
is_moderator: boolean(),
|
||||||
super_users: boolean(),
|
super_users: boolean(),
|
||||||
|
exclude_service_users: boolean(),
|
||||||
followers: User.t(),
|
followers: User.t(),
|
||||||
friends: User.t(),
|
friends: User.t(),
|
||||||
recipients_from_activity: [String.t()],
|
recipients_from_activity: [String.t()],
|
||||||
|
@ -88,6 +89,10 @@ defp compose_query({key, value}, query)
|
||||||
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
|
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp compose_query({:exclude_service_users, _}, query) do
|
||||||
|
where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
|
||||||
|
end
|
||||||
|
|
||||||
defp compose_query({key, value}, query)
|
defp compose_query({key, value}, query)
|
||||||
when key in @equal_criteria and not_empty_string(value) do
|
when key in @equal_criteria and not_empty_string(value) do
|
||||||
where(query, [u], ^[{key, value}])
|
where(query, [u], ^[{key, value}])
|
||||||
|
@ -98,7 +103,7 @@ defp compose_query({key, values}, query) when key in @contains_criteria and is_l
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
|
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
|
||||||
Enum.reduce(tags, query, &prepare_tag_criteria/2)
|
where(query, [u], fragment("? && ?", u.tags, ^tags))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compose_query({:is_admin, _}, query) do
|
defp compose_query({:is_admin, _}, query) do
|
||||||
|
@ -192,10 +197,6 @@ defp compose_query({:limit, limit}, query) do
|
||||||
|
|
||||||
defp compose_query(_unsupported_param, query), do: query
|
defp compose_query(_unsupported_param, query), do: query
|
||||||
|
|
||||||
defp prepare_tag_criteria(tag, query) do
|
|
||||||
or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp location_query(query, local) do
|
defp location_query(query, local) do
|
||||||
where(query, [u], u.local == ^local)
|
where(query, [u], u.local == ^local)
|
||||||
|> where([u], not is_nil(u.nickname))
|
|> where([u], not is_nil(u.nickname))
|
||||||
|
|
|
@ -170,12 +170,6 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
||||||
|
|
||||||
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
|
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
|
||||||
|
|
||||||
Notification.create_notifications(activity)
|
|
||||||
|
|
||||||
conversation = create_or_bump_conversation(activity, map["actor"])
|
|
||||||
participations = get_participations(conversation)
|
|
||||||
stream_out(activity)
|
|
||||||
stream_out_participations(participations)
|
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
%Activity{} = activity ->
|
%Activity{} = activity ->
|
||||||
|
@ -198,6 +192,15 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notify_and_stream(activity) do
|
||||||
|
Notification.create_notifications(activity)
|
||||||
|
|
||||||
|
conversation = create_or_bump_conversation(activity, activity.actor)
|
||||||
|
participations = get_participations(conversation)
|
||||||
|
stream_out(activity)
|
||||||
|
stream_out_participations(participations)
|
||||||
|
end
|
||||||
|
|
||||||
defp create_or_bump_conversation(activity, actor) do
|
defp create_or_bump_conversation(activity, actor) do
|
||||||
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
|
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
|
||||||
%User{} = user <- User.get_cached_by_ap_id(actor),
|
%User{} = user <- User.get_cached_by_ap_id(actor),
|
||||||
|
@ -274,6 +277,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
|
||||||
_ <- increase_poll_votes_if_vote(create_data),
|
_ <- increase_poll_votes_if_vote(create_data),
|
||||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -301,6 +305,7 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
|
||||||
additional
|
additional
|
||||||
),
|
),
|
||||||
{:ok, activity} <- insert(listen_data, local),
|
{:ok, activity} <- insert(listen_data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
end
|
end
|
||||||
|
@ -325,6 +330,7 @@ def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
||||||
|> Utils.maybe_put("id", activity_id),
|
|> Utils.maybe_put("id", activity_id),
|
||||||
{:ok, activity} <- insert(data, local),
|
{:ok, activity} <- insert(data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
end
|
end
|
||||||
|
@ -344,83 +350,12 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
||||||
},
|
},
|
||||||
data <- Utils.maybe_put(data, "id", activity_id),
|
data <- Utils.maybe_put(data, "id", activity_id),
|
||||||
{:ok, activity} <- insert(data, local),
|
{:ok, activity} <- insert(data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
|
|
||||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
|
||||||
def react_with_emoji(user, object, emoji, options \\ []) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_react_with_emoji(user, object, emoji, options) do
|
|
||||||
with local <- Keyword.get(options, :local, true),
|
|
||||||
activity_id <- Keyword.get(options, :activity_id, nil),
|
|
||||||
true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
|
|
||||||
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
|
|
||||||
{:ok, activity} <- insert(reaction_data, local),
|
|
||||||
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity, object}
|
|
||||||
else
|
|
||||||
false -> {:error, false}
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
|
|
||||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
|
||||||
def unreact_with_emoji(user, reaction_id, options \\ []) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_unreact_with_emoji(user, reaction_id, options) do
|
|
||||||
with local <- Keyword.get(options, :local, true),
|
|
||||||
activity_id <- Keyword.get(options, :activity_id, nil),
|
|
||||||
user_ap_id <- user.ap_id,
|
|
||||||
%Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
|
|
||||||
object <- Object.normalize(reaction_activity),
|
|
||||||
unreact_data <- make_undo_data(user, reaction_activity, activity_id),
|
|
||||||
{:ok, activity} <- insert(unreact_data, local),
|
|
||||||
{:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity, object}
|
|
||||||
else
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
|
|
||||||
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
|
|
||||||
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_unlike(actor, object, activity_id, local) do
|
|
||||||
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
|
|
||||||
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
|
|
||||||
{:ok, unlike_activity} <- insert(unlike_data, local),
|
|
||||||
{:ok, _activity} <- Repo.delete(like_activity),
|
|
||||||
{:ok, object} <- remove_like_from_object(like_activity, object),
|
|
||||||
:ok <- maybe_federate(unlike_activity) do
|
|
||||||
{:ok, unlike_activity, like_activity, object}
|
|
||||||
else
|
|
||||||
nil -> {:ok, object}
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
|
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
|
||||||
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
{:ok, Activity.t(), Object.t()} | {:error, any()}
|
||||||
def announce(
|
def announce(
|
||||||
|
@ -442,6 +377,7 @@ defp do_announce(user, object, activity_id, local, public) do
|
||||||
announce_data <- make_announce_data(user, object, activity_id, public),
|
announce_data <- make_announce_data(user, object, activity_id, public),
|
||||||
{:ok, activity} <- insert(announce_data, local),
|
{:ok, activity} <- insert(announce_data, local),
|
||||||
{:ok, object} <- add_announce_to_object(activity, object),
|
{:ok, object} <- add_announce_to_object(activity, object),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity, object}
|
{:ok, activity, object}
|
||||||
else
|
else
|
||||||
|
@ -450,34 +386,6 @@ defp do_announce(user, object, activity_id, local, public) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
|
|
||||||
{:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
|
|
||||||
def unannounce(
|
|
||||||
%User{} = actor,
|
|
||||||
%Object{} = object,
|
|
||||||
activity_id \\ nil,
|
|
||||||
local \\ true
|
|
||||||
) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_unannounce(actor, object, activity_id, local) do
|
|
||||||
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
|
|
||||||
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
|
||||||
{:ok, unannounce_activity} <- insert(unannounce_data, local),
|
|
||||||
:ok <- maybe_federate(unannounce_activity),
|
|
||||||
{:ok, _activity} <- Repo.delete(announce_activity),
|
|
||||||
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
|
|
||||||
{:ok, unannounce_activity, object}
|
|
||||||
else
|
|
||||||
nil -> {:ok, object}
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||||
{:ok, Activity.t()} | {:error, any()}
|
{:ok, Activity.t()} | {:error, any()}
|
||||||
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||||
|
@ -490,6 +398,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||||
defp do_follow(follower, followed, activity_id, local) do
|
defp do_follow(follower, followed, activity_id, local) do
|
||||||
with data <- make_follow_data(follower, followed, activity_id),
|
with data <- make_follow_data(follower, followed, activity_id),
|
||||||
{:ok, activity} <- insert(data, local),
|
{:ok, activity} <- insert(data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -511,6 +420,7 @@ defp do_unfollow(follower, followed, activity_id, local) do
|
||||||
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
|
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
|
||||||
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
|
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
|
||||||
{:ok, activity} <- insert(unfollow_data, local),
|
{:ok, activity} <- insert(unfollow_data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -519,67 +429,6 @@ defp do_unfollow(follower, followed, activity_id, local) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
|
|
||||||
def delete(entity, options \\ []) do
|
|
||||||
with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
|
|
||||||
with data <- %{
|
|
||||||
"to" => [follower_address],
|
|
||||||
"type" => "Delete",
|
|
||||||
"actor" => ap_id,
|
|
||||||
"object" => %{"type" => "Person", "id" => ap_id}
|
|
||||||
},
|
|
||||||
{:ok, activity} <- insert(data, true, true, true),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, user}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
|
|
||||||
local = Keyword.get(options, :local, true)
|
|
||||||
activity_id = Keyword.get(options, :activity_id, nil)
|
|
||||||
actor = Keyword.get(options, :actor, actor)
|
|
||||||
|
|
||||||
user = User.get_cached_by_ap_id(actor)
|
|
||||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
|
||||||
|
|
||||||
with create_activity <- Activity.get_create_by_object_ap_id(id),
|
|
||||||
data <-
|
|
||||||
%{
|
|
||||||
"type" => "Delete",
|
|
||||||
"actor" => actor,
|
|
||||||
"object" => id,
|
|
||||||
"to" => to,
|
|
||||||
"deleted_activity_id" => create_activity && create_activity.id
|
|
||||||
}
|
|
||||||
|> maybe_put("id", activity_id),
|
|
||||||
{:ok, activity} <- insert(data, local, false),
|
|
||||||
{:ok, object, _create_activity} <- Object.delete(object),
|
|
||||||
stream_out_participations(object, user),
|
|
||||||
_ <- decrease_replies_count_if_reply(object),
|
|
||||||
{:ok, _actor} <- decrease_note_count_if_public(user, object),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
{:error, error} ->
|
|
||||||
Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
|
|
||||||
activity =
|
|
||||||
ap_id
|
|
||||||
|> Activity.Queries.by_object_id()
|
|
||||||
|> Activity.Queries.by_type("Delete")
|
|
||||||
|> Repo.one()
|
|
||||||
|
|
||||||
{:ok, activity}
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
|
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||||
{:ok, Activity.t()} | {:error, any()}
|
{:ok, Activity.t()} | {:error, any()}
|
||||||
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
|
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
|
||||||
|
@ -601,6 +450,7 @@ defp do_block(blocker, blocked, activity_id, local) do
|
||||||
with true <- outgoing_blocks,
|
with true <- outgoing_blocks,
|
||||||
block_data <- make_block_data(blocker, blocked, activity_id),
|
block_data <- make_block_data(blocker, blocked, activity_id),
|
||||||
{:ok, activity} <- insert(block_data, local),
|
{:ok, activity} <- insert(block_data, local),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -608,27 +458,6 @@ defp do_block(blocker, blocked, activity_id, local) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
|
|
||||||
{:ok, Activity.t()} | {:error, any()} | nil
|
|
||||||
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
|
|
||||||
with {:ok, result} <-
|
|
||||||
Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_unblock(blocker, blocked, activity_id, local) do
|
|
||||||
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
|
|
||||||
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
|
|
||||||
{:ok, activity} <- insert(unblock_data, local),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
nil -> nil
|
|
||||||
{:error, error} -> Repo.rollback(error)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
|
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||||
def flag(
|
def flag(
|
||||||
%{
|
%{
|
||||||
|
@ -655,6 +484,7 @@ def flag(
|
||||||
with flag_data <- make_flag_data(params, additional),
|
with flag_data <- make_flag_data(params, additional),
|
||||||
{:ok, activity} <- insert(flag_data, local),
|
{:ok, activity} <- insert(flag_data, local),
|
||||||
{:ok, stripped_activity} <- strip_report_status_data(activity),
|
{:ok, stripped_activity} <- strip_report_status_data(activity),
|
||||||
|
_ <- notify_and_stream(activity),
|
||||||
:ok <- maybe_federate(stripped_activity) do
|
:ok <- maybe_federate(stripped_activity) do
|
||||||
User.all_superusers()
|
User.all_superusers()
|
||||||
|> Enum.filter(fn user -> not is_nil(user.email) end)
|
|> Enum.filter(fn user -> not is_nil(user.email) end)
|
||||||
|
@ -678,7 +508,8 @@ def move(%User{} = origin, %User{} = target, local \\ true) do
|
||||||
}
|
}
|
||||||
|
|
||||||
with true <- origin.ap_id in target.also_known_as,
|
with true <- origin.ap_id in target.also_known_as,
|
||||||
{:ok, activity} <- insert(params, local) do
|
{:ok, activity} <- insert(params, local),
|
||||||
|
_ <- notify_and_stream(activity) do
|
||||||
maybe_federate(activity)
|
maybe_federate(activity)
|
||||||
|
|
||||||
BackgroundWorker.enqueue("move_following", %{
|
BackgroundWorker.enqueue("move_following", %{
|
||||||
|
@ -1530,21 +1361,34 @@ def fetch_follow_information_for_user(user) do
|
||||||
defp normalize_counter(counter) when is_integer(counter), do: counter
|
defp normalize_counter(counter) when is_integer(counter), do: counter
|
||||||
defp normalize_counter(_), do: 0
|
defp normalize_counter(_), do: 0
|
||||||
|
|
||||||
defp maybe_update_follow_information(data) do
|
def maybe_update_follow_information(user_data) do
|
||||||
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
|
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
|
||||||
{:ok, info} <- fetch_follow_information_for_user(data) do
|
{_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
|
||||||
info = Map.merge(data[:info] || %{}, info)
|
{_, true} <-
|
||||||
Map.put(data, :info, info)
|
{:collections_available,
|
||||||
|
!!(user_data[:following_address] && user_data[:follower_address])},
|
||||||
|
{:ok, info} <-
|
||||||
|
fetch_follow_information_for_user(user_data) do
|
||||||
|
info = Map.merge(user_data[:info] || %{}, info)
|
||||||
|
|
||||||
|
user_data
|
||||||
|
|> Map.put(:info, info)
|
||||||
else
|
else
|
||||||
|
{:user_type_check, false} ->
|
||||||
|
user_data
|
||||||
|
|
||||||
|
{:collections_available, false} ->
|
||||||
|
user_data
|
||||||
|
|
||||||
{:enabled, false} ->
|
{:enabled, false} ->
|
||||||
data
|
user_data
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
Logger.error(
|
Logger.error(
|
||||||
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
|
"Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
data
|
user_data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -34,12 +34,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
EnsureAuthenticatedPlug,
|
EnsureAuthenticatedPlug,
|
||||||
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
|
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Note: :following and :followers must be served even without authentication (as via :api)
|
||||||
plug(
|
plug(
|
||||||
EnsureAuthenticatedPlug
|
EnsureAuthenticatedPlug
|
||||||
when action in [:read_inbox, :update_outbox, :whoami, :upload_media, :following, :followers]
|
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -395,7 +396,10 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
||||||
|> json(err)
|
|> json(err)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
|
defp handle_user_activity(
|
||||||
|
%User{} = user,
|
||||||
|
%{"type" => "Create", "object" => %{"type" => "Note"}} = params
|
||||||
|
) do
|
||||||
object =
|
object =
|
||||||
params["object"]
|
params["object"]
|
||||||
|> Map.merge(Map.take(params, ["to", "cc"]))
|
|> Map.merge(Map.take(params, ["to", "cc"]))
|
||||||
|
@ -414,7 +418,8 @@ defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
|
||||||
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
|
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
|
||||||
with %Object{} = object <- Object.normalize(params["object"]),
|
with %Object{} = object <- Object.normalize(params["object"]),
|
||||||
true <- user.is_moderator || user.ap_id == object.data["actor"],
|
true <- user.is_moderator || user.ap_id == object.data["actor"],
|
||||||
{:ok, delete} <- ActivityPub.delete(object) do
|
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
||||||
|
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
||||||
{:ok, delete}
|
{:ok, delete}
|
||||||
else
|
else
|
||||||
_ -> {:error, dgettext("errors", "Can't delete object")}
|
_ -> {:error, dgettext("errors", "Can't delete object")}
|
||||||
|
|
|
@ -10,8 +10,71 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
|
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||||
|
def emoji_react(actor, object, emoji) do
|
||||||
|
with {:ok, data, meta} <- object_action(actor, object) do
|
||||||
|
data =
|
||||||
|
data
|
||||||
|
|> Map.put("content", emoji)
|
||||||
|
|> Map.put("type", "EmojiReact")
|
||||||
|
|
||||||
|
{:ok, data, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
|
||||||
|
def undo(actor, object) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"id" => Utils.generate_activity_id(),
|
||||||
|
"actor" => actor.ap_id,
|
||||||
|
"type" => "Undo",
|
||||||
|
"object" => object.data["id"],
|
||||||
|
"to" => object.data["to"] || [],
|
||||||
|
"cc" => object.data["cc"] || []
|
||||||
|
}, []}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
|
||||||
|
def delete(actor, object_id) do
|
||||||
|
object = Object.normalize(object_id, false)
|
||||||
|
|
||||||
|
user = !object && User.get_cached_by_ap_id(object_id)
|
||||||
|
|
||||||
|
to =
|
||||||
|
case {object, user} do
|
||||||
|
{%Object{}, _} ->
|
||||||
|
# We are deleting an object, address everyone who was originally mentioned
|
||||||
|
(object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||||
|
|
||||||
|
{_, %User{follower_address: follower_address}} ->
|
||||||
|
# We are deleting a user, address the followers of that user
|
||||||
|
[follower_address]
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
"id" => Utils.generate_activity_id(),
|
||||||
|
"actor" => actor.ap_id,
|
||||||
|
"object" => object_id,
|
||||||
|
"to" => to,
|
||||||
|
"type" => "Delete"
|
||||||
|
}, []}
|
||||||
|
end
|
||||||
|
|
||||||
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
def like(actor, object) do
|
def like(actor, object) do
|
||||||
|
with {:ok, data, meta} <- object_action(actor, object) do
|
||||||
|
data =
|
||||||
|
data
|
||||||
|
|> Map.put("type", "Like")
|
||||||
|
|
||||||
|
{:ok, data, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||||
|
defp object_action(actor, object) do
|
||||||
object_actor = User.get_cached_by_ap_id(object.data["actor"])
|
object_actor = User.get_cached_by_ap_id(object.data["actor"])
|
||||||
|
|
||||||
# Address the actor of the object, and our actor's follower collection if the post is public.
|
# Address the actor of the object, and our actor's follower collection if the post is public.
|
||||||
|
@ -33,7 +96,6 @@ def like(actor, object) do
|
||||||
%{
|
%{
|
||||||
"id" => Utils.generate_activity_id(),
|
"id" => Utils.generate_activity_id(),
|
||||||
"actor" => actor.ap_id,
|
"actor" => actor.ap_id,
|
||||||
"type" => "Like",
|
|
||||||
"object" => object.data["id"],
|
"object" => object.data["id"],
|
||||||
"to" => to,
|
"to" => to,
|
||||||
"cc" => cc,
|
"cc" => cc,
|
||||||
|
|
|
@ -11,11 +11,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
|
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
|
||||||
|
|
||||||
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||||
def validate(object, meta)
|
def validate(object, meta)
|
||||||
|
|
||||||
|
def validate(%{"type" => "Undo"} = object, meta) do
|
||||||
|
with {:ok, object} <-
|
||||||
|
object
|
||||||
|
|> UndoValidator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
object = stringify_keys(object)
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(%{"type" => "Delete"} = object, meta) do
|
||||||
|
with cng <- DeleteValidator.cast_and_validate(object),
|
||||||
|
do_not_federate <- DeleteValidator.do_not_federate?(cng),
|
||||||
|
{:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
|
||||||
|
object = stringify_keys(object)
|
||||||
|
meta = Keyword.put(meta, :do_not_federate, do_not_federate)
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate(%{"type" => "Like"} = object, meta) do
|
def validate(%{"type" => "Like"} = object, meta) do
|
||||||
with {:ok, object} <-
|
with {:ok, object} <-
|
||||||
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
|
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
@ -24,13 +48,35 @@ def validate(%{"type" => "Like"} = object, meta) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate(%{"type" => "EmojiReact"} = object, meta) do
|
||||||
|
with {:ok, object} <-
|
||||||
|
object
|
||||||
|
|> EmojiReactValidator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
object = stringify_keys(object |> Map.from_struct())
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stringify_keys(%{__struct__: _} = object) do
|
||||||
|
object
|
||||||
|
|> Map.from_struct()
|
||||||
|
|> stringify_keys
|
||||||
|
end
|
||||||
|
|
||||||
def stringify_keys(object) do
|
def stringify_keys(object) do
|
||||||
object
|
object
|
||||||
|> Map.new(fn {key, val} -> {to_string(key), val} end)
|
|> Map.new(fn {key, val} -> {to_string(key), val} end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_actor(object) do
|
||||||
|
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
|
||||||
|
User.get_or_fetch_by_ap_id(actor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_actor_and_object(object) do
|
def fetch_actor_and_object(object) do
|
||||||
User.get_or_fetch_by_ap_id(object["actor"])
|
fetch_actor(object)
|
||||||
Object.normalize(object["object"])
|
Object.normalize(object["object"])
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,33 @@
|
||||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
def validate_actor_presence(cng, field_name \\ :actor) do
|
def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
|
||||||
|
non_empty =
|
||||||
|
fields
|
||||||
|
|> Enum.map(fn field -> get_field(cng, field) end)
|
||||||
|
|> Enum.any?(fn
|
||||||
|
[] -> false
|
||||||
|
_ -> true
|
||||||
|
end)
|
||||||
|
|
||||||
|
if non_empty do
|
||||||
|
cng
|
||||||
|
else
|
||||||
|
fields
|
||||||
|
|> Enum.reduce(cng, fn field, cng ->
|
||||||
|
cng
|
||||||
|
|> add_error(field, "no recipients in any field")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_actor_presence(cng, options \\ []) do
|
||||||
|
field_name = Keyword.get(options, :field_name, :actor)
|
||||||
|
|
||||||
cng
|
cng
|
||||||
|> validate_change(field_name, fn field_name, actor ->
|
|> validate_change(field_name, fn field_name, actor ->
|
||||||
if User.get_cached_by_ap_id(actor) do
|
if User.get_cached_by_ap_id(actor) do
|
||||||
|
@ -19,14 +42,39 @@ def validate_actor_presence(cng, field_name \\ :actor) do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_object_presence(cng, field_name \\ :object) do
|
def validate_object_presence(cng, options \\ []) do
|
||||||
|
field_name = Keyword.get(options, :field_name, :object)
|
||||||
|
allowed_types = Keyword.get(options, :allowed_types, false)
|
||||||
|
|
||||||
cng
|
cng
|
||||||
|> validate_change(field_name, fn field_name, object ->
|
|> validate_change(field_name, fn field_name, object_id ->
|
||||||
if Object.get_cached_by_ap_id(object) do
|
object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)
|
||||||
[]
|
|
||||||
else
|
cond do
|
||||||
|
!object ->
|
||||||
[{field_name, "can't find object"}]
|
[{field_name, "can't find object"}]
|
||||||
|
|
||||||
|
object && allowed_types && object.data["type"] not in allowed_types ->
|
||||||
|
[{field_name, "object not in allowed types"}]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_object_or_user_presence(cng, options \\ []) do
|
||||||
|
field_name = Keyword.get(options, :field_name, :object)
|
||||||
|
options = Keyword.put(options, :field_name, field_name)
|
||||||
|
|
||||||
|
actor_cng =
|
||||||
|
cng
|
||||||
|
|> validate_actor_presence(options)
|
||||||
|
|
||||||
|
object_cng =
|
||||||
|
cng
|
||||||
|
|> validate_object_presence(options)
|
||||||
|
|
||||||
|
if actor_cng.valid?, do: actor_cng, else: object_cng
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:id, Types.ObjectID, primary_key: true)
|
||||||
|
field(:type, :string)
|
||||||
|
field(:actor, Types.ObjectID)
|
||||||
|
field(:to, Types.Recipients, default: [])
|
||||||
|
field(:cc, Types.Recipients, default: [])
|
||||||
|
field(:deleted_activity_id, Types.ObjectID)
|
||||||
|
field(:object, Types.ObjectID)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_data(data) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> cast(data, __schema__(:fields))
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_deleted_activity_id(cng) do
|
||||||
|
object =
|
||||||
|
cng
|
||||||
|
|> get_field(:object)
|
||||||
|
|
||||||
|
with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
|
||||||
|
cng
|
||||||
|
|> put_change(:deleted_activity_id, id)
|
||||||
|
else
|
||||||
|
_ -> cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@deletable_types ~w{
|
||||||
|
Answer
|
||||||
|
Article
|
||||||
|
Audio
|
||||||
|
Event
|
||||||
|
Note
|
||||||
|
Page
|
||||||
|
Question
|
||||||
|
Video
|
||||||
|
}
|
||||||
|
def validate_data(cng) do
|
||||||
|
cng
|
||||||
|
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||||
|
|> validate_inclusion(:type, ["Delete"])
|
||||||
|
|> validate_actor_presence()
|
||||||
|
|> validate_deletion_rights()
|
||||||
|
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|
||||||
|
|> add_deleted_activity_id()
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_not_federate?(cng) do
|
||||||
|
!same_domain?(cng)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp same_domain?(cng) do
|
||||||
|
actor_uri =
|
||||||
|
cng
|
||||||
|
|> get_field(:actor)
|
||||||
|
|> URI.parse()
|
||||||
|
|
||||||
|
object_uri =
|
||||||
|
cng
|
||||||
|
|> get_field(:object)
|
||||||
|
|> URI.parse()
|
||||||
|
|
||||||
|
object_uri.host == actor_uri.host
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_deletion_rights(cng) do
|
||||||
|
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
|
||||||
|
|
||||||
|
if User.superuser?(actor) || same_domain?(cng) do
|
||||||
|
cng
|
||||||
|
else
|
||||||
|
cng
|
||||||
|
|> add_error(:actor, "is not allowed to delete object")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_and_validate(data) do
|
||||||
|
data
|
||||||
|
|> cast_data
|
||||||
|
|> validate_data
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:id, Types.ObjectID, primary_key: true)
|
||||||
|
field(:type, :string)
|
||||||
|
field(:object, Types.ObjectID)
|
||||||
|
field(:actor, Types.ObjectID)
|
||||||
|
field(:context, :string)
|
||||||
|
field(:content, :string)
|
||||||
|
field(:to, {:array, :string}, default: [])
|
||||||
|
field(:cc, {:array, :string}, default: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_and_validate(data) do
|
||||||
|
data
|
||||||
|
|> cast_data()
|
||||||
|
|> validate_data()
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_data(data) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> changeset(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(struct, data) do
|
||||||
|
struct
|
||||||
|
|> cast(data, __schema__(:fields))
|
||||||
|
|> fix_after_cast()
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_after_cast(cng) do
|
||||||
|
cng
|
||||||
|
|> fix_context()
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_context(cng) do
|
||||||
|
object = get_field(cng, :object)
|
||||||
|
|
||||||
|
with nil <- get_field(cng, :context),
|
||||||
|
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
|
||||||
|
cng
|
||||||
|
|> put_change(:context, context)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_emoji(cng) do
|
||||||
|
content = get_field(cng, :content)
|
||||||
|
|
||||||
|
if Pleroma.Emoji.is_unicode_emoji?(content) do
|
||||||
|
cng
|
||||||
|
else
|
||||||
|
cng
|
||||||
|
|> add_error(:content, "must be a single character emoji")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_data(data_cng) do
|
||||||
|
data_cng
|
||||||
|
|> validate_inclusion(:type, ["EmojiReact"])
|
||||||
|
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|
||||||
|
|> validate_actor_presence()
|
||||||
|
|> validate_object_presence()
|
||||||
|
|> validate_emoji()
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,7 @@
|
||||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
|
||||||
|
@ -19,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
|
||||||
field(:object, Types.ObjectID)
|
field(:object, Types.ObjectID)
|
||||||
field(:actor, Types.ObjectID)
|
field(:actor, Types.ObjectID)
|
||||||
field(:context, :string)
|
field(:context, :string)
|
||||||
field(:to, {:array, :string})
|
field(:to, Types.Recipients, default: [])
|
||||||
field(:cc, {:array, :string})
|
field(:cc, Types.Recipients, default: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def cast_and_validate(data) do
|
def cast_and_validate(data) do
|
||||||
|
@ -31,7 +32,48 @@ def cast_and_validate(data) do
|
||||||
|
|
||||||
def cast_data(data) do
|
def cast_data(data) do
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
|
|> changeset(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(struct, data) do
|
||||||
|
struct
|
||||||
|
|> cast(data, __schema__(:fields))
|
||||||
|
|> fix_after_cast()
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_after_cast(cng) do
|
||||||
|
cng
|
||||||
|
|> fix_recipients()
|
||||||
|
|> fix_context()
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_context(cng) do
|
||||||
|
object = get_field(cng, :object)
|
||||||
|
|
||||||
|
with nil <- get_field(cng, :context),
|
||||||
|
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
|
||||||
|
cng
|
||||||
|
|> put_change(:context, context)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_recipients(cng) do
|
||||||
|
to = get_field(cng, :to)
|
||||||
|
cc = get_field(cng, :cc)
|
||||||
|
object = get_field(cng, :object)
|
||||||
|
|
||||||
|
with {[], []} <- {to, cc},
|
||||||
|
%Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
|
||||||
|
{:ok, actor} <- Types.ObjectID.cast(actor) do
|
||||||
|
cng
|
||||||
|
|> put_change(:to, [actor])
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
cng
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_data(data_cng) do
|
def validate_data(data_cng) do
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
|
||||||
|
use Ecto.Type
|
||||||
|
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
|
||||||
|
|
||||||
|
def type, do: {:array, ObjectID}
|
||||||
|
|
||||||
|
def cast(object) when is_binary(object) do
|
||||||
|
cast([object])
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast(data) when is_list(data) do
|
||||||
|
data
|
||||||
|
|> Enum.reduce({:ok, []}, fn element, acc ->
|
||||||
|
case {acc, ObjectID.cast(element)} do
|
||||||
|
{:error, _} -> :error
|
||||||
|
{_, :error} -> :error
|
||||||
|
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast(_) do
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump(data) do
|
||||||
|
{:ok, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
def load(data) do
|
||||||
|
{:ok, data}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|
||||||
|
use Ecto.Schema
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
|
|
||||||
|
@primary_key false
|
||||||
|
|
||||||
|
embedded_schema do
|
||||||
|
field(:id, Types.ObjectID, primary_key: true)
|
||||||
|
field(:type, :string)
|
||||||
|
field(:object, Types.ObjectID)
|
||||||
|
field(:actor, Types.ObjectID)
|
||||||
|
field(:to, {:array, :string}, default: [])
|
||||||
|
field(:cc, {:array, :string}, default: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_and_validate(data) do
|
||||||
|
data
|
||||||
|
|> cast_data()
|
||||||
|
|> validate_data()
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast_data(data) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> changeset(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(struct, data) do
|
||||||
|
struct
|
||||||
|
|> cast(data, __schema__(:fields))
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_data(data_cng) do
|
||||||
|
data_cng
|
||||||
|
|> validate_inclusion(:type, ["Undo"])
|
||||||
|
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|
||||||
|
|> validate_actor_presence()
|
||||||
|
|> validate_object_presence()
|
||||||
|
|> validate_undo_rights()
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_undo_rights(cng) do
|
||||||
|
actor = get_field(cng, :actor)
|
||||||
|
object = get_field(cng, :object)
|
||||||
|
|
||||||
|
with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object),
|
||||||
|
true <- object_actor != actor do
|
||||||
|
cng
|
||||||
|
|> add_error(:actor, "not the same as object actor")
|
||||||
|
else
|
||||||
|
_ -> cng
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,20 +4,33 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.ActivityPub.Pipeline do
|
defmodule Pleroma.Web.ActivityPub.Pipeline do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Repo
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.MRF
|
alias Pleroma.Web.ActivityPub.MRF
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||||
alias Pleroma.Web.ActivityPub.SideEffects
|
alias Pleroma.Web.ActivityPub.SideEffects
|
||||||
alias Pleroma.Web.Federator
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
@spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
|
@spec common_pipeline(map(), keyword()) ::
|
||||||
|
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
|
||||||
def common_pipeline(object, meta) do
|
def common_pipeline(object, meta) do
|
||||||
|
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
|
||||||
|
{:ok, value} ->
|
||||||
|
value
|
||||||
|
|
||||||
|
{:error, e} ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_common_pipeline(object, meta) do
|
||||||
with {_, {:ok, validated_object, meta}} <-
|
with {_, {:ok, validated_object, meta}} <-
|
||||||
{:validate_object, ObjectValidator.validate(object, meta)},
|
{:validate_object, ObjectValidator.validate(object, meta)},
|
||||||
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
|
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
|
||||||
{_, {:ok, %Activity{} = activity, meta}} <-
|
{_, {:ok, activity, meta}} <-
|
||||||
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
|
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
|
||||||
{_, {:ok, %Activity{} = activity, meta}} <-
|
{_, {:ok, activity, meta}} <-
|
||||||
{:execute_side_effects, SideEffects.handle(activity, meta)},
|
{:execute_side_effects, SideEffects.handle(activity, meta)},
|
||||||
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
|
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
|
||||||
{:ok, activity, meta}
|
{:ok, activity, meta}
|
||||||
|
@ -27,9 +40,13 @@ def common_pipeline(object, meta) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_federate(activity, meta) do
|
defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
|
||||||
|
|
||||||
|
defp maybe_federate(%Activity{} = activity, meta) do
|
||||||
with {:ok, local} <- Keyword.fetch(meta, :local) do
|
with {:ok, local} <- Keyword.fetch(meta, :local) do
|
||||||
if local do
|
do_not_federate = meta[:do_not_federate]
|
||||||
|
|
||||||
|
if !do_not_federate && local do
|
||||||
Federator.publish(activity)
|
Federator.publish(activity)
|
||||||
{:ok, :federated}
|
{:ok, :federated}
|
||||||
else
|
else
|
||||||
|
|
|
@ -5,8 +5,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
liked object, a `Follow` activity will add the user to the follower
|
liked object, a `Follow` activity will add the user to the follower
|
||||||
collection, and so on.
|
collection, and so on.
|
||||||
"""
|
"""
|
||||||
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.Repo
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
|
||||||
def handle(object, meta \\ [])
|
def handle(object, meta \\ [])
|
||||||
|
@ -15,21 +19,115 @@ def handle(object, meta \\ [])
|
||||||
# - Add like to object
|
# - Add like to object
|
||||||
# - Set up notification
|
# - Set up notification
|
||||||
def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
||||||
{:ok, result} =
|
|
||||||
Pleroma.Repo.transaction(fn ->
|
|
||||||
liked_object = Object.get_by_ap_id(object.data["object"])
|
liked_object = Object.get_by_ap_id(object.data["object"])
|
||||||
Utils.add_like_to_object(object, liked_object)
|
Utils.add_like_to_object(object, liked_object)
|
||||||
|
|
||||||
Notification.create_notifications(object)
|
Notification.create_notifications(object)
|
||||||
|
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end)
|
end
|
||||||
|
|
||||||
result
|
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
|
||||||
|
with undone_object <- Activity.get_by_ap_id(undone_object),
|
||||||
|
:ok <- handle_undoing(undone_object) do
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tasks this handles:
|
||||||
|
# - Add reaction to object
|
||||||
|
# - Set up notification
|
||||||
|
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
|
||||||
|
reacted_object = Object.get_by_ap_id(object.data["object"])
|
||||||
|
Utils.add_emoji_reaction_to_object(object, reacted_object)
|
||||||
|
|
||||||
|
Notification.create_notifications(object)
|
||||||
|
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tasks this handles:
|
||||||
|
# - Delete and unpins the create activity
|
||||||
|
# - Replace object with Tombstone
|
||||||
|
# - Set up notification
|
||||||
|
# - Reduce the user note count
|
||||||
|
# - Reduce the reply count
|
||||||
|
# - Stream out the activity
|
||||||
|
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
|
||||||
|
deleted_object =
|
||||||
|
Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
|
||||||
|
|
||||||
|
result =
|
||||||
|
case deleted_object do
|
||||||
|
%Object{} ->
|
||||||
|
with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
|
||||||
|
%User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
|
||||||
|
User.remove_pinnned_activity(user, activity)
|
||||||
|
|
||||||
|
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
|
||||||
|
|
||||||
|
if in_reply_to = deleted_object.data["inReplyTo"] do
|
||||||
|
Object.decrease_replies_count(in_reply_to)
|
||||||
|
end
|
||||||
|
|
||||||
|
ActivityPub.stream_out(object)
|
||||||
|
ActivityPub.stream_out_participations(deleted_object, user)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
%User{} ->
|
||||||
|
with {:ok, _} <- User.delete(deleted_object) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if result == :ok do
|
||||||
|
Notification.create_notifications(object)
|
||||||
|
{:ok, object, meta}
|
||||||
|
else
|
||||||
|
{:error, result}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Nothing to do
|
# Nothing to do
|
||||||
def handle(object, meta) do
|
def handle(object, meta) do
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||||
|
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||||
|
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
|
||||||
|
{:ok, _} <- Repo.delete(object) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
|
||||||
|
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
|
||||||
|
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
|
||||||
|
{:ok, _} <- Repo.delete(object) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
|
||||||
|
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||||
|
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
|
||||||
|
{:ok, _} <- Repo.delete(object) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_undoing(
|
||||||
|
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
|
||||||
|
) do
|
||||||
|
with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
|
||||||
|
%User{} = blocked <- User.get_cached_by_ap_id(blocked),
|
||||||
|
{:ok, _} <- User.unblock(blocker, blocked),
|
||||||
|
{:ok, _} <- Repo.delete(object) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
@ -657,44 +656,16 @@ def handle_incoming(
|
||||||
|> handle_incoming(options)
|
|> handle_incoming(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(%{"type" => "Like"} = data, _options) do
|
def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
|
||||||
with {_, {:ok, cast_data_sym}} <-
|
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||||
{:casting_data,
|
{:ok, activity, _meta} <-
|
||||||
data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
|
Pipeline.common_pipeline(data, local: false) do
|
||||||
cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
|
|
||||||
:ok <- ObjectValidator.fetch_actor_and_object(cast_data),
|
|
||||||
{_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
|
|
||||||
{_, {:ok, cast_data}} <-
|
|
||||||
{:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
|
|
||||||
{_, {:ok, activity, _meta}} <-
|
|
||||||
{:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
|
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{
|
|
||||||
"type" => "EmojiReact",
|
|
||||||
"object" => object_id,
|
|
||||||
"actor" => _actor,
|
|
||||||
"id" => id,
|
|
||||||
"content" => emoji
|
|
||||||
} = data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
|
||||||
{:ok, object} <- get_obj_helper(object_id),
|
|
||||||
{:ok, activity, _object} <-
|
|
||||||
ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
||||||
_options
|
_options
|
||||||
|
@ -743,55 +714,12 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: We presently assume that any actor on the same origin domain as the object being
|
|
||||||
# deleted has the rights to delete that object. A better way to validate whether or not
|
|
||||||
# the object should be deleted is to refetch the object URI, which should return either
|
|
||||||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
|
||||||
# place.
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
|
%{"type" => "Delete"} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
object_id = Utils.get_ap_id(object_id)
|
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||||
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
|
||||||
{:ok, object} <- get_obj_helper(object_id),
|
|
||||||
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
|
||||||
{:ok, activity} <-
|
|
||||||
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
|
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
|
||||||
nil ->
|
|
||||||
case User.get_cached_by_ap_id(object_id) do
|
|
||||||
%User{ap_id: ^actor} = user ->
|
|
||||||
User.delete(user)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
|
|
||||||
_e ->
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"object" => %{"type" => "Announce", "object" => object_id},
|
|
||||||
"actor" => _actor,
|
|
||||||
"id" => id
|
|
||||||
} = data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
|
||||||
{:ok, object} <- get_obj_helper(object_id),
|
|
||||||
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -817,75 +745,13 @@ def handle_incoming(
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{
|
%{
|
||||||
"type" => "Undo",
|
"type" => "Undo",
|
||||||
"object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
|
"object" => %{"type" => type}
|
||||||
"actor" => _actor,
|
|
||||||
"id" => id
|
|
||||||
} = data,
|
} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
)
|
||||||
with actor <- Containment.get_actor(data),
|
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||||
{:ok, activity, _} <-
|
|
||||||
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
|
|
||||||
activity_id: id,
|
|
||||||
local: false
|
|
||||||
) do
|
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"object" => %{"type" => "Block", "object" => blocked},
|
|
||||||
"actor" => blocker,
|
|
||||||
"id" => id
|
|
||||||
} = _data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
|
|
||||||
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
|
|
||||||
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
|
|
||||||
User.unblock(blocker, blocked)
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
|
|
||||||
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
|
|
||||||
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
|
|
||||||
User.unfollow(blocker, blocked)
|
|
||||||
User.block(blocker, blocked)
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_incoming(
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"object" => %{"type" => "Like", "object" => object_id},
|
|
||||||
"actor" => _actor,
|
|
||||||
"id" => id
|
|
||||||
} = data,
|
|
||||||
_options
|
|
||||||
) do
|
|
||||||
with actor <- Containment.get_actor(data),
|
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
|
||||||
{:ok, object} <- get_obj_helper(object_id),
|
|
||||||
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
|
|
||||||
{:ok, activity}
|
|
||||||
else
|
|
||||||
_e -> :error
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -907,6 +773,21 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_incoming(
|
||||||
|
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
|
||||||
|
_options
|
||||||
|
) do
|
||||||
|
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
|
||||||
|
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
|
||||||
|
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
|
||||||
|
User.unfollow(blocker, blocked)
|
||||||
|
User.block(blocker, blocked)
|
||||||
|
{:ok, activity}
|
||||||
|
else
|
||||||
|
_e -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{
|
%{
|
||||||
"type" => "Move",
|
"type" => "Move",
|
||||||
|
@ -1203,6 +1084,10 @@ def set_conversation(object) do
|
||||||
Map.put(object, "conversation", object["context"])
|
Map.put(object, "conversation", object["context"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_sensitive(%{"sensitive" => true} = object) do
|
||||||
|
object
|
||||||
|
end
|
||||||
|
|
||||||
def set_sensitive(object) do
|
def set_sensitive(object) do
|
||||||
tags = object["tag"] || []
|
tags = object["tag"] || []
|
||||||
Map.put(object, "sensitive", "nsfw" in tags)
|
Map.put(object, "sensitive", "nsfw" in tags)
|
||||||
|
@ -1296,45 +1181,4 @@ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||||
def maybe_fix_user_url(data), do: data
|
def maybe_fix_user_url(data), do: data
|
||||||
|
|
||||||
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
|
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
|
||||||
|
|
||||||
defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
|
|
||||||
do: {:ok, data}
|
|
||||||
|
|
||||||
defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
|
|
||||||
with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
|
|
||||||
{:ok, Map.put(data, "context", context)}
|
|
||||||
else
|
|
||||||
_ ->
|
|
||||||
{:error, :no_context}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_context_presence(_) do
|
|
||||||
{:error, :no_context}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
|
|
||||||
do: {:ok, data}
|
|
||||||
|
|
||||||
defp ensure_recipients_presence(%{"object" => object} = data) do
|
|
||||||
case Object.normalize(object) do
|
|
||||||
%{data: %{"actor" => actor}} ->
|
|
||||||
data =
|
|
||||||
data
|
|
||||||
|> Map.put("to", [actor])
|
|
||||||
|> Map.put("cc", data["cc"] || [])
|
|
||||||
|
|
||||||
{:ok, data}
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
{:error, :no_object}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, :no_actor}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_recipients_presence(_) do
|
|
||||||
{:error, :no_object}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -512,7 +512,7 @@ def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
|
||||||
#### Announce-related helpers
|
#### Announce-related helpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Retruns an existing announce activity if the notice has already been announced
|
Returns an existing announce activity if the notice has already been announced
|
||||||
"""
|
"""
|
||||||
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
|
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
|
||||||
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
|
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
|
||||||
|
@ -562,45 +562,6 @@ def make_announce_data(
|
||||||
|> maybe_put("id", activity_id)
|
|> maybe_put("id", activity_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Make unannounce activity data for the given actor and object
|
|
||||||
"""
|
|
||||||
def make_unannounce_data(
|
|
||||||
%User{ap_id: ap_id} = user,
|
|
||||||
%Activity{data: %{"context" => context, "object" => object}} = activity,
|
|
||||||
activity_id
|
|
||||||
) do
|
|
||||||
object = Object.normalize(object)
|
|
||||||
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"actor" => ap_id,
|
|
||||||
"object" => activity.data,
|
|
||||||
"to" => [user.follower_address, object.data["actor"]],
|
|
||||||
"cc" => [Pleroma.Constants.as_public()],
|
|
||||||
"context" => context
|
|
||||||
}
|
|
||||||
|> maybe_put("id", activity_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def make_unlike_data(
|
|
||||||
%User{ap_id: ap_id} = user,
|
|
||||||
%Activity{data: %{"context" => context, "object" => object}} = activity,
|
|
||||||
activity_id
|
|
||||||
) do
|
|
||||||
object = Object.normalize(object)
|
|
||||||
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"actor" => ap_id,
|
|
||||||
"object" => activity.data,
|
|
||||||
"to" => [user.follower_address, object.data["actor"]],
|
|
||||||
"cc" => [Pleroma.Constants.as_public()],
|
|
||||||
"context" => context
|
|
||||||
}
|
|
||||||
|> maybe_put("id", activity_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def make_undo_data(
|
def make_undo_data(
|
||||||
%User{ap_id: actor, follower_address: follower_address},
|
%User{ap_id: actor, follower_address: follower_address},
|
||||||
%Activity{
|
%Activity{
|
||||||
|
@ -688,16 +649,6 @@ def make_block_data(blocker, blocked, activity_id) do
|
||||||
|> maybe_put("id", activity_id)
|
|> maybe_put("id", activity_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
|
|
||||||
%{
|
|
||||||
"type" => "Undo",
|
|
||||||
"actor" => blocker.ap_id,
|
|
||||||
"to" => [blocked.ap_id],
|
|
||||||
"object" => block_activity.data
|
|
||||||
}
|
|
||||||
|> maybe_put("id", activity_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
#### Create-related helpers
|
#### Create-related helpers
|
||||||
|
|
||||||
def make_create_data(params, additional) do
|
def make_create_data(params, additional) do
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.ConfigDB
|
alias Pleroma.ConfigDB
|
||||||
|
alias Pleroma.MFA
|
||||||
alias Pleroma.ModerationLog
|
alias Pleroma.ModerationLog
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.ReportNote
|
alias Pleroma.ReportNote
|
||||||
|
@ -17,6 +18,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.UserInviteToken
|
alias Pleroma.UserInviteToken
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Relay
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.AdminAPI.AccountView
|
alias Pleroma.Web.AdminAPI.AccountView
|
||||||
|
@ -59,6 +62,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
:right_add,
|
:right_add,
|
||||||
:right_add_multiple,
|
:right_add_multiple,
|
||||||
:right_delete,
|
:right_delete,
|
||||||
|
:disable_mfa,
|
||||||
:right_delete_multiple,
|
:right_delete_multiple,
|
||||||
:update_user_credentials
|
:update_user_credentials
|
||||||
]
|
]
|
||||||
|
@ -93,7 +97,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read:statuses"], admin: true}
|
%{scopes: ["read:statuses"], admin: true}
|
||||||
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
|
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -133,23 +137,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
|
|
||||||
action_fallback(:errors)
|
action_fallback(:errors)
|
||||||
|
|
||||||
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
def user_delete(conn, %{"nickname" => nickname}) do
|
||||||
user = User.get_cached_by_nickname(nickname)
|
user_delete(conn, %{"nicknames" => [nickname]})
|
||||||
User.delete(user)
|
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
actor: admin,
|
|
||||||
subject: [user],
|
|
||||||
action: "delete"
|
|
||||||
})
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> json(nickname)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
users =
|
||||||
User.delete(users)
|
nicknames
|
||||||
|
|> Enum.map(&User.get_cached_by_nickname/1)
|
||||||
|
|
||||||
|
users
|
||||||
|
|> Enum.each(fn user ->
|
||||||
|
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
|
||||||
|
Pipeline.common_pipeline(delete_data, local: true)
|
||||||
|
end)
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: admin,
|
actor: admin,
|
||||||
|
@ -392,29 +393,12 @@ def list_users(conn, params) do
|
||||||
email: params["email"]
|
email: params["email"]
|
||||||
}
|
}
|
||||||
|
|
||||||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
|
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
|
||||||
{:ok, users, count} <- filter_service_users(users, count),
|
json(
|
||||||
do:
|
conn,
|
||||||
conn
|
AccountView.render("index.json", users: users, count: count, page_size: page_size)
|
||||||
|> json(
|
|
||||||
AccountView.render("index.json",
|
|
||||||
users: users,
|
|
||||||
count: count,
|
|
||||||
page_size: page_size
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_service_users(users, count) do
|
|
||||||
filtered_users = Enum.reject(users, &service_user?/1)
|
|
||||||
count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
|
|
||||||
|
|
||||||
{:ok, filtered_users, count}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp service_user?(user) do
|
|
||||||
String.match?(user.ap_id, ~r/.*\/relay$/) or
|
|
||||||
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@filters ~w(local external active deactivated is_admin is_moderator)
|
@filters ~w(local external active deactivated is_admin is_moderator)
|
||||||
|
@ -692,6 +676,18 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
|
||||||
json_response(conn, :no_content, "")
|
json_response(conn, :no_content, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Disable mfa for user's account."
|
||||||
|
def disable_mfa(conn, %{"nickname" => nickname}) do
|
||||||
|
case User.get_by_nickname(nickname) do
|
||||||
|
%User{} = user ->
|
||||||
|
MFA.disable(user)
|
||||||
|
json(conn, nickname)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Show a given user's credentials"
|
@doc "Show a given user's credentials"
|
||||||
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||||
|
@ -837,6 +833,16 @@ def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
|
||||||
|> render("index.json", %{activities: activities, as: :activity})
|
|> render("index.json", %{activities: activities, as: :activity})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_show(conn, %{"id" => id}) do
|
||||||
|
with %Activity{} = activity <- Activity.get_by_id(id) do
|
||||||
|
conn
|
||||||
|
|> put_view(StatusView)
|
||||||
|
|> render("show.json", %{activity: activity})
|
||||||
|
else
|
||||||
|
_ -> errors(conn, {:error, :not_found})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
|
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
|
||||||
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
|
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
|
||||||
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
|
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
|
||||||
|
|
|
@ -21,6 +21,7 @@ def user(params \\ %{}) do
|
||||||
query =
|
query =
|
||||||
params
|
params
|
||||||
|> Map.drop([:page, :page_size])
|
|> Map.drop([:page, :page_size])
|
||||||
|
|> Map.put(:exclude_service_users, true)
|
||||||
|> User.Query.build()
|
|> User.Query.build()
|
||||||
|> order_by([u], u.nickname)
|
|> order_by([u], u.nickname)
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,12 @@ def spec do
|
||||||
password: %OpenApiSpex.OAuthFlow{
|
password: %OpenApiSpex.OAuthFlow{
|
||||||
authorizationUrl: "/oauth/authorize",
|
authorizationUrl: "/oauth/authorize",
|
||||||
tokenUrl: "/oauth/token",
|
tokenUrl: "/oauth/token",
|
||||||
scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
|
scopes: %{
|
||||||
|
"read" => "read",
|
||||||
|
"write" => "write",
|
||||||
|
"follow" => "follow",
|
||||||
|
"push" => "push"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
|
||||||
|
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
|
||||||
|
@moduledoc """
|
||||||
|
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
|
||||||
|
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
|
||||||
|
The main difference is ignoring unexpected query params instead of throwing
|
||||||
|
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
|
||||||
|
to disable this behavior. Also, the default rendering error module
|
||||||
|
is `Pleroma.Web.ApiSpec.RenderError`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour Plug
|
||||||
|
|
||||||
|
alias Plug.Conn
|
||||||
|
|
||||||
|
@impl Plug
|
||||||
|
def init(opts) do
|
||||||
|
opts
|
||||||
|
|> Map.new()
|
||||||
|
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Plug
|
||||||
|
def call(%{private: %{open_api_spex: private_data}} = conn, %{
|
||||||
|
operation_id: operation_id,
|
||||||
|
render_error: render_error
|
||||||
|
}) do
|
||||||
|
spec = private_data.spec
|
||||||
|
operation = private_data.operation_lookup[operation_id]
|
||||||
|
|
||||||
|
content_type =
|
||||||
|
case Conn.get_req_header(conn, "content-type") do
|
||||||
|
[header_value | _] ->
|
||||||
|
header_value
|
||||||
|
|> String.split(";")
|
||||||
|
|> List.first()
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private_data = Map.put(private_data, :operation_id, operation_id)
|
||||||
|
conn = Conn.put_private(conn, :open_api_spex, private_data)
|
||||||
|
|
||||||
|
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
conn
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
opts = render_error.init(reason)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> render_error.call(opts)
|
||||||
|
|> Plug.Conn.halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(
|
||||||
|
%{
|
||||||
|
private: %{
|
||||||
|
phoenix_controller: controller,
|
||||||
|
phoenix_action: action,
|
||||||
|
open_api_spex: private_data
|
||||||
|
}
|
||||||
|
} = conn,
|
||||||
|
opts
|
||||||
|
) do
|
||||||
|
operation =
|
||||||
|
case private_data.operation_lookup[{controller, action}] do
|
||||||
|
nil ->
|
||||||
|
operation_id = controller.open_api_operation(action).operationId
|
||||||
|
operation = private_data.operation_lookup[operation_id]
|
||||||
|
|
||||||
|
operation_lookup =
|
||||||
|
private_data.operation_lookup
|
||||||
|
|> Map.put({controller, action}, operation)
|
||||||
|
|
||||||
|
OpenApiSpex.Plug.Cache.adapter().put(
|
||||||
|
private_data.spec_module,
|
||||||
|
{private_data.spec, operation_lookup}
|
||||||
|
)
|
||||||
|
|
||||||
|
operation
|
||||||
|
|
||||||
|
operation ->
|
||||||
|
operation
|
||||||
|
end
|
||||||
|
|
||||||
|
if operation.operationId do
|
||||||
|
call(conn, Map.put(opts, :operation_id, operation.operationId))
|
||||||
|
else
|
||||||
|
raise "operationId was not found in action API spec"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
|
||||||
|
|
||||||
|
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
|
||||||
|
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
|
||||||
|
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
{:ok, conn}
|
||||||
|
|
||||||
|
# Remove unexpected query params and cast/validate again
|
||||||
|
{:error, errors} ->
|
||||||
|
query_params =
|
||||||
|
Enum.reduce(errors, conn.query_params, fn
|
||||||
|
%{reason: :unexpected_field, name: name, path: [name]}, params ->
|
||||||
|
Map.delete(params, name)
|
||||||
|
|
||||||
|
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
|
||||||
|
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
|
||||||
|
update_in(params, path, &List.delete(&1, value))
|
||||||
|
|
||||||
|
_, params ->
|
||||||
|
params
|
||||||
|
end)
|
||||||
|
|
||||||
|
conn = %Conn{conn | query_params: query_params}
|
||||||
|
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp list_items_to_string(list) do
|
||||||
|
Enum.map(list, fn
|
||||||
|
i when is_atom(i) -> to_string(i)
|
||||||
|
i -> i
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
|
||||||
|
end
|
|
@ -41,8 +41,8 @@ def pagination_params do
|
||||||
Operation.parameter(
|
Operation.parameter(
|
||||||
:limit,
|
:limit,
|
||||||
:query,
|
:query,
|
||||||
%Schema{type: :integer, default: 20, maximum: 40},
|
%Schema{type: :integer, default: 20},
|
||||||
"Limit"
|
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.ActorType
|
alias Pleroma.Web.ApiSpec.Schemas.ActorType
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.List
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
|
||||||
|
@ -555,11 +556,12 @@ defp update_creadentials_request do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp array_of_accounts do
|
def array_of_accounts do
|
||||||
%Schema{
|
%Schema{
|
||||||
title: "ArrayOfAccounts",
|
title: "ArrayOfAccounts",
|
||||||
type: :array,
|
type: :array,
|
||||||
items: Account
|
items: Account,
|
||||||
|
example: [Account.schema().example]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -646,28 +648,12 @@ defp mute_request do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp list do
|
|
||||||
%Schema{
|
|
||||||
title: "List",
|
|
||||||
description: "Response schema for a list",
|
|
||||||
type: :object,
|
|
||||||
properties: %{
|
|
||||||
id: %Schema{type: :string},
|
|
||||||
title: %Schema{type: :string}
|
|
||||||
},
|
|
||||||
example: %{
|
|
||||||
"id" => "123",
|
|
||||||
"title" => "my list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp array_of_lists do
|
defp array_of_lists do
|
||||||
%Schema{
|
%Schema{
|
||||||
title: "ArrayOfLists",
|
title: "ArrayOfLists",
|
||||||
description: "Response schema for lists",
|
description: "Response schema for lists",
|
||||||
type: :array,
|
type: :array,
|
||||||
items: list(),
|
items: List,
|
||||||
example: [
|
example: [
|
||||||
%{"id" => "123", "title" => "my list"},
|
%{"id" => "123", "title" => "my list"},
|
||||||
%{"id" => "1337", "title" => "anotehr list"}
|
%{"id" => "1337", "title" => "anotehr list"}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.ConversationOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Conversations"],
|
||||||
|
summary: "Show conversation",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
operationId: "ConversationController.index",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:recipients,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :array, items: FlakeID},
|
||||||
|
"Only return conversations with the given recipients (a list of user ids)"
|
||||||
|
)
|
||||||
|
| pagination_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of Conversation", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Conversation,
|
||||||
|
example: [Conversation.schema().example]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_as_read_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Conversations"],
|
||||||
|
summary: "Mark as read",
|
||||||
|
operationId: "ConversationController.mark_as_read",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
],
|
||||||
|
security: [%{"oAuth" => ["write:conversations"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Conversation", "application/json", Conversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,227 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["apps"],
|
||||||
|
summary: "View all filters",
|
||||||
|
operationId: "FilterController.index",
|
||||||
|
security: [%{"oAuth" => ["read:filters"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Filters", "application/json", array_of_filters())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["apps"],
|
||||||
|
summary: "Create a filter",
|
||||||
|
operationId: "FilterController.create",
|
||||||
|
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||||
|
security: [%{"oAuth" => ["write:filters"]}],
|
||||||
|
responses: %{200 => Operation.response("Filter", "application/json", filter())}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["apps"],
|
||||||
|
summary: "View all filters",
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "FilterController.show",
|
||||||
|
security: [%{"oAuth" => ["read:filters"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Filter", "application/json", filter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["apps"],
|
||||||
|
summary: "Update a filter",
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "FilterController.update",
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||||
|
security: [%{"oAuth" => ["write:filters"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Filter", "application/json", filter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["apps"],
|
||||||
|
summary: "Remove a filter",
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "FilterController.delete",
|
||||||
|
security: [%{"oAuth" => ["write:filters"]}],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Filter", "application/json", %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "Empty object"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter do
|
||||||
|
%Schema{
|
||||||
|
title: "Filter",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||||
|
context: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||||
|
description: "The contexts in which the filter should be applied."
|
||||||
|
},
|
||||||
|
expires_at: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :"date-time",
|
||||||
|
description:
|
||||||
|
"When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
irreversible: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Should matching entities in home and notifications be dropped by the server?"
|
||||||
|
},
|
||||||
|
whole_word: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Should the filter consider word boundaries?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "5580",
|
||||||
|
"phrase" => "@twitter.com",
|
||||||
|
"context" => [
|
||||||
|
"home",
|
||||||
|
"notifications",
|
||||||
|
"public",
|
||||||
|
"thread"
|
||||||
|
],
|
||||||
|
"whole_word" => false,
|
||||||
|
"expires_at" => nil,
|
||||||
|
"irreversible" => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp array_of_filters do
|
||||||
|
%Schema{
|
||||||
|
title: "ArrayOfFilters",
|
||||||
|
description: "Array of Filters",
|
||||||
|
type: :array,
|
||||||
|
items: filter(),
|
||||||
|
example: [
|
||||||
|
%{
|
||||||
|
"id" => "5580",
|
||||||
|
"phrase" => "@twitter.com",
|
||||||
|
"context" => [
|
||||||
|
"home",
|
||||||
|
"notifications",
|
||||||
|
"public",
|
||||||
|
"thread"
|
||||||
|
],
|
||||||
|
"whole_word" => false,
|
||||||
|
"expires_at" => nil,
|
||||||
|
"irreversible" => true
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
"id" => "6191",
|
||||||
|
"phrase" => ":eurovision2019:",
|
||||||
|
"context" => [
|
||||||
|
"home"
|
||||||
|
],
|
||||||
|
"whole_word" => true,
|
||||||
|
"expires_at" => "2019-05-21T13:47:31.333Z",
|
||||||
|
"irreversible" => false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_request do
|
||||||
|
%Schema{
|
||||||
|
title: "FilterCreateRequest",
|
||||||
|
allOf: [
|
||||||
|
update_request(),
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
irreversible: %Schema{
|
||||||
|
type: :bolean,
|
||||||
|
description:
|
||||||
|
"Should the server irreversibly drop matching entities from home and notifications?",
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
example: %{
|
||||||
|
"phrase" => "knights",
|
||||||
|
"context" => ["home"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "FilterUpdateRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
phrase: %Schema{type: :string, description: "The text to be filtered"},
|
||||||
|
context: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
|
||||||
|
description:
|
||||||
|
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
||||||
|
},
|
||||||
|
irreversible: %Schema{
|
||||||
|
type: :bolean,
|
||||||
|
description:
|
||||||
|
"Should the server irreversibly drop matching entities from home and notifications?"
|
||||||
|
},
|
||||||
|
whole_word: %Schema{
|
||||||
|
type: :bolean,
|
||||||
|
description: "Consider word boundaries?",
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
# TODO: probably should implement filter expiration
|
||||||
|
# expires_in: %Schema{
|
||||||
|
# type: :string,
|
||||||
|
# format: :"date-time",
|
||||||
|
# description:
|
||||||
|
# "ISO 8601 Datetime for when the filter expires. Otherwise,
|
||||||
|
# null for a filter that doesn't expire."
|
||||||
|
# }
|
||||||
|
},
|
||||||
|
required: [:phrase, :context],
|
||||||
|
example: %{
|
||||||
|
"phrase" => "knights",
|
||||||
|
"context" => ["home"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Follow Requests"],
|
||||||
|
summary: "Pending Follows",
|
||||||
|
security: [%{"oAuth" => ["read:follows", "follow"]}],
|
||||||
|
operationId: "FollowRequestController.index",
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of Account", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account,
|
||||||
|
example: [Account.schema().example]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Follow Requests"],
|
||||||
|
summary: "Accept Follow",
|
||||||
|
operationId: "FollowRequestController.authorize",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Follow Requests"],
|
||||||
|
summary: "Reject Follow",
|
||||||
|
operationId: "FollowRequestController.reject",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Relationship", "application/json", AccountRelationship)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, :string, "Conversation ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Instance"],
|
||||||
|
summary: "Fetch instance",
|
||||||
|
description: "Information about the server",
|
||||||
|
operationId: "InstanceController.show",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Instance", "application/json", instance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def peers_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Instance"],
|
||||||
|
summary: "List of known hosts",
|
||||||
|
operationId: "InstanceController.peers",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Array of domains", "application/json", array_of_domains())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
uri: %Schema{type: :string, description: "The domain name of the instance"},
|
||||||
|
title: %Schema{type: :string, description: "The title of the website"},
|
||||||
|
description: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Admin-defined description of the Pleroma site"
|
||||||
|
},
|
||||||
|
version: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "The version of Pleroma installed on the instance"
|
||||||
|
},
|
||||||
|
email: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "An email that may be contacted for any inquiries",
|
||||||
|
format: :email
|
||||||
|
},
|
||||||
|
urls: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "URLs of interest for clients apps",
|
||||||
|
properties: %{
|
||||||
|
streaming_api: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Websockets address for push streaming"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stats: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "Statistics about how much information the instance contains",
|
||||||
|
properties: %{
|
||||||
|
user_count: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Users registered on this instance"
|
||||||
|
},
|
||||||
|
status_count: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Statuses authored by users on instance"
|
||||||
|
},
|
||||||
|
domain_count: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Domains federated with this instance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
thumbnail: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Banner image for the website",
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
languages: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Primary langauges of the website and its staff"
|
||||||
|
},
|
||||||
|
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
|
||||||
|
# Extra (not present in Mastodon):
|
||||||
|
max_toot_chars: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: ": Posts character limit (CW/Subject included in the counter)"
|
||||||
|
},
|
||||||
|
poll_limits: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "A map with poll limits for local polls",
|
||||||
|
properties: %{
|
||||||
|
max_options: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Maximum number of options."
|
||||||
|
},
|
||||||
|
max_option_chars: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Maximum number of characters per option."
|
||||||
|
},
|
||||||
|
min_expiration: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Minimum expiration time (in seconds)."
|
||||||
|
},
|
||||||
|
max_expiration: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "Maximum expiration time (in seconds)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
upload_limit: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
description: "File size limit of uploads (except for avatar, background, banner)"
|
||||||
|
},
|
||||||
|
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||||
|
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||||
|
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"avatar_upload_limit" => 2_000_000,
|
||||||
|
"background_upload_limit" => 4_000_000,
|
||||||
|
"banner_upload_limit" => 4_000_000,
|
||||||
|
"description" => "A Pleroma instance, an alternative fediverse server",
|
||||||
|
"email" => "lain@lain.com",
|
||||||
|
"languages" => ["en"],
|
||||||
|
"max_toot_chars" => 5000,
|
||||||
|
"poll_limits" => %{
|
||||||
|
"max_expiration" => 31_536_000,
|
||||||
|
"max_option_chars" => 200,
|
||||||
|
"max_options" => 20,
|
||||||
|
"min_expiration" => 0
|
||||||
|
},
|
||||||
|
"registrations" => false,
|
||||||
|
"stats" => %{
|
||||||
|
"domain_count" => 2996,
|
||||||
|
"status_count" => 15_802,
|
||||||
|
"user_count" => 5
|
||||||
|
},
|
||||||
|
"thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
|
||||||
|
"title" => "lain.com",
|
||||||
|
"upload_limit" => 16_000_000,
|
||||||
|
"uri" => "https://lain.com",
|
||||||
|
"urls" => %{
|
||||||
|
"streaming_api" => "wss://lain.com"
|
||||||
|
},
|
||||||
|
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp array_of_domains do
|
||||||
|
%Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
example: ["pleroma.site", "lain.com", "bikeshed.party"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,188 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.ListOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.List
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Show user's lists",
|
||||||
|
description: "Fetch all lists that the user owns",
|
||||||
|
security: [%{"oAuth" => ["read:lists"]}],
|
||||||
|
operationId: "ListController.index",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Array of List", "application/json", array_of_lists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Create a list",
|
||||||
|
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||||
|
operationId: "ListController.create",
|
||||||
|
requestBody: create_update_request(),
|
||||||
|
security: [%{"oAuth" => ["write:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("List", "application/json", List),
|
||||||
|
400 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Show a single list",
|
||||||
|
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
|
||||||
|
operationId: "ListController.show",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["read:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("List", "application/json", List),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Update a list",
|
||||||
|
description: "Change the title of a list",
|
||||||
|
operationId: "ListController.update",
|
||||||
|
parameters: [id_param()],
|
||||||
|
requestBody: create_update_request(),
|
||||||
|
security: [%{"oAuth" => ["write:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("List", "application/json", List),
|
||||||
|
422 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Delete a list",
|
||||||
|
operationId: "ListController.delete",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["write:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_accounts_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "View accounts in list",
|
||||||
|
operationId: "ListController.list_accounts",
|
||||||
|
parameters: [id_param()],
|
||||||
|
security: [%{"oAuth" => ["read:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of Account", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_to_list_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Add accounts to list",
|
||||||
|
description: "Add accounts to the given list.",
|
||||||
|
operationId: "ListController.add_to_list",
|
||||||
|
parameters: [id_param()],
|
||||||
|
requestBody: add_remove_accounts_request(),
|
||||||
|
security: [%{"oAuth" => ["write:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_list_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Lists"],
|
||||||
|
summary: "Remove accounts from list",
|
||||||
|
operationId: "ListController.remove_from_list",
|
||||||
|
parameters: [id_param()],
|
||||||
|
requestBody: add_remove_accounts_request(),
|
||||||
|
security: [%{"oAuth" => ["write:lists"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp array_of_lists do
|
||||||
|
%Schema{
|
||||||
|
title: "ArrayOfLists",
|
||||||
|
description: "Response schema for lists",
|
||||||
|
type: :array,
|
||||||
|
items: List,
|
||||||
|
example: [
|
||||||
|
%{"id" => "123", "title" => "my list"},
|
||||||
|
%{"id" => "1337", "title" => "another list"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, :string, "List ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_update_request do
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{
|
||||||
|
description: "POST body for creating or updating a List",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
title: %Schema{type: :string, description: "List title"}
|
||||||
|
},
|
||||||
|
required: [:title]
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_remove_accounts_request do
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{
|
||||||
|
description: "POST body for adding/removing accounts to/from a List",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
|
||||||
|
},
|
||||||
|
required: [:account_ids]
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,140 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.MarkerOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Markers"],
|
||||||
|
summary: "Get saved timeline position",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
operationId: "MarkerController.index",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:timeline,
|
||||||
|
:query,
|
||||||
|
%Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string, enum: ["home", "notifications"]}
|
||||||
|
},
|
||||||
|
"Array of markers to fetch. If not provided, an empty object will be returned."
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Marker", "application/json", response()),
|
||||||
|
403 => Operation.response("Error", "application/json", api_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def upsert_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Markers"],
|
||||||
|
summary: "Save position in timeline",
|
||||||
|
operationId: "MarkerController.upsert",
|
||||||
|
requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
|
||||||
|
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Marker", "application/json", response()),
|
||||||
|
403 => Operation.response("Error", "application/json", api_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp marker do
|
||||||
|
%Schema{
|
||||||
|
title: "Marker",
|
||||||
|
description: "Schema for a marker",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
last_read_id: %Schema{type: :string},
|
||||||
|
version: %Schema{type: :integer},
|
||||||
|
updated_at: %Schema{type: :string},
|
||||||
|
pleroma: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
unread_count: %Schema{type: :integer}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"last_read_id" => "35098814",
|
||||||
|
"version" => 361,
|
||||||
|
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||||
|
"pleroma" => %{"unread_count" => 5}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp response do
|
||||||
|
%Schema{
|
||||||
|
title: "MarkersResponse",
|
||||||
|
description: "Response schema for markers",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
notifications: %Schema{allOf: [marker()], nullable: true},
|
||||||
|
home: %Schema{allOf: [marker()], nullable: true}
|
||||||
|
},
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
example: %{
|
||||||
|
"notifications" => %{
|
||||||
|
"last_read_id" => "35098814",
|
||||||
|
"version" => 361,
|
||||||
|
"updated_at" => "2019-11-26T22:37:25.239Z",
|
||||||
|
"pleroma" => %{"unread_count" => 0}
|
||||||
|
},
|
||||||
|
"home" => %{
|
||||||
|
"last_read_id" => "103206604258487607",
|
||||||
|
"version" => 468,
|
||||||
|
"updated_at" => "2019-11-26T22:37:25.235Z",
|
||||||
|
"pleroma" => %{"unread_count" => 10}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp upsert_request do
|
||||||
|
%Schema{
|
||||||
|
title: "MarkersUpsertRequest",
|
||||||
|
description: "Request schema for marker upsert",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
notifications: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
last_read_id: %Schema{type: :string}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
home: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
last_read_id: %Schema{type: :string}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"home" => %{
|
||||||
|
"last_read_id" => "103194548672408537",
|
||||||
|
"version" => 462,
|
||||||
|
"updated_at" => "2019-11-24T19:39:39.337Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp api_error do
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{error: %Schema{type: :string}}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -178,7 +178,16 @@ defp notification do
|
||||||
defp notification_type do
|
defp notification_type do
|
||||||
%Schema{
|
%Schema{
|
||||||
type: :string,
|
type: :string,
|
||||||
enum: ["follow", "favourite", "reblog", "mention", "poll", "pleroma:emoji_reaction", "move"],
|
enum: [
|
||||||
|
"follow",
|
||||||
|
"favourite",
|
||||||
|
"reblog",
|
||||||
|
"mention",
|
||||||
|
"poll",
|
||||||
|
"pleroma:emoji_reaction",
|
||||||
|
"move",
|
||||||
|
"follow_request"
|
||||||
|
],
|
||||||
description: """
|
description: """
|
||||||
The type of event that resulted in the notification.
|
The type of event that resulted in the notification.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.PollOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Polls"],
|
||||||
|
summary: "View a poll",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "PollController.show",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Poll", "application/json", Poll),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def vote_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Polls"],
|
||||||
|
summary: "Vote on a poll",
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "PollController.vote",
|
||||||
|
requestBody: vote_request(),
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Poll", "application/json", Poll),
|
||||||
|
422 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp vote_request do
|
||||||
|
request_body(
|
||||||
|
"Parameters",
|
||||||
|
%Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
choices: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :integer},
|
||||||
|
description: "Array of own votes containing index for each option (starting from 0)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:choices]
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
example: %{
|
||||||
|
"choices" => [0, 1, 2]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Scheduled Statuses"],
|
||||||
|
summary: "View scheduled statuses",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: pagination_params(),
|
||||||
|
operationId: "ScheduledActivity.index",
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Array of ScheduledStatus", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: ScheduledStatus
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Scheduled Statuses"],
|
||||||
|
summary: "View a single scheduled status",
|
||||||
|
security: [%{"oAuth" => ["read:statuses"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "ScheduledActivity.show",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Scheduled Statuses"],
|
||||||
|
summary: "Schedule a status",
|
||||||
|
operationId: "ScheduledActivity.update",
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
requestBody:
|
||||||
|
request_body("Parameters", %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
scheduled_at: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :"date-time",
|
||||||
|
description:
|
||||||
|
"ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Scheduled Statuses"],
|
||||||
|
summary: "Cancel a scheduled status",
|
||||||
|
security: [%{"oAuth" => ["write:statuses"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "ScheduledActivity.delete",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(:id, :path, FlakeID, "Poll ID",
|
||||||
|
example: "123",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,207 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.SearchOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||||
|
|
||||||
|
import Pleroma.Web.ApiSpec.Helpers
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_search_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Search"],
|
||||||
|
summary: "Search for matching accounts by username or display name",
|
||||||
|
operationId: "SearchController.account_search",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:limit,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer, default: 40},
|
||||||
|
"Maximum number of results"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:resolve,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Attempt WebFinger lookup. Use this when `q` is an exact address."
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:following,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Only include accounts that the user is following"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response(
|
||||||
|
"Array of Account",
|
||||||
|
"application/json",
|
||||||
|
AccountOperation.array_of_accounts()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Search"],
|
||||||
|
summary: "Search results",
|
||||||
|
security: [%{"oAuth" => ["read:search"]}],
|
||||||
|
operationId: "SearchController.search",
|
||||||
|
deprecated: true,
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:account_id,
|
||||||
|
:query,
|
||||||
|
FlakeID,
|
||||||
|
"If provided, statuses returned will be authored only by this account"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:type,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
|
||||||
|
"Search type"
|
||||||
|
),
|
||||||
|
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
|
||||||
|
Operation.parameter(
|
||||||
|
:resolve,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Attempt WebFinger lookup"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:following,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Only include accounts that the user is following"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:offset,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer},
|
||||||
|
"Offset"
|
||||||
|
)
|
||||||
|
| pagination_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Results", "application/json", results())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def search2_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Search"],
|
||||||
|
summary: "Search results",
|
||||||
|
security: [%{"oAuth" => ["read:search"]}],
|
||||||
|
operationId: "SearchController.search2",
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(
|
||||||
|
:account_id,
|
||||||
|
:query,
|
||||||
|
FlakeID,
|
||||||
|
"If provided, statuses returned will be authored only by this account"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:type,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
|
||||||
|
"Search type"
|
||||||
|
),
|
||||||
|
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
|
||||||
|
required: true
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:resolve,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Attempt WebFinger lookup"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:following,
|
||||||
|
:query,
|
||||||
|
%Schema{allOf: [BooleanLike], default: false},
|
||||||
|
"Only include accounts that the user is following"
|
||||||
|
)
|
||||||
|
| pagination_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Results", "application/json", results2())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp results2 do
|
||||||
|
%Schema{
|
||||||
|
title: "SearchResults",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
accounts: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account,
|
||||||
|
description: "Accounts which match the given query"
|
||||||
|
},
|
||||||
|
statuses: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Status,
|
||||||
|
description: "Statuses which match the given query"
|
||||||
|
},
|
||||||
|
hashtags: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Tag,
|
||||||
|
description: "Hashtags which match the given query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"accounts" => [Account.schema().example],
|
||||||
|
"statuses" => [Status.schema().example],
|
||||||
|
"hashtags" => [Tag.schema().example]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp results do
|
||||||
|
%Schema{
|
||||||
|
title: "SearchResults",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
accounts: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account,
|
||||||
|
description: "Accounts which match the given query"
|
||||||
|
},
|
||||||
|
statuses: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Status,
|
||||||
|
description: "Statuses which match the given query"
|
||||||
|
},
|
||||||
|
hashtags: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description: "Hashtags which match the given query"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"accounts" => [Account.schema().example],
|
||||||
|
"statuses" => [Status.schema().example],
|
||||||
|
"hashtags" => ["cofe"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,188 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Push Subscriptions"],
|
||||||
|
summary: "Subscribe to push notifications",
|
||||||
|
description:
|
||||||
|
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
|
||||||
|
operationId: "SubscriptionController.create",
|
||||||
|
security: [%{"oAuth" => ["push"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||||
|
400 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Push Subscriptions"],
|
||||||
|
summary: "Get current subscription",
|
||||||
|
description: "View the PushSubscription currently associated with this access token.",
|
||||||
|
operationId: "SubscriptionController.show",
|
||||||
|
security: [%{"oAuth" => ["push"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||||
|
403 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Push Subscriptions"],
|
||||||
|
summary: "Change types of notifications",
|
||||||
|
description:
|
||||||
|
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
|
||||||
|
operationId: "SubscriptionController.update",
|
||||||
|
security: [%{"oAuth" => ["push"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
|
||||||
|
403 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Push Subscriptions"],
|
||||||
|
summary: "Remove current subscription",
|
||||||
|
description: "Removes the current Web Push API subscription.",
|
||||||
|
operationId: "SubscriptionController.delete",
|
||||||
|
security: [%{"oAuth" => ["push"]}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
|
||||||
|
403 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_request do
|
||||||
|
%Schema{
|
||||||
|
title: "SubscriptionCreateRequest",
|
||||||
|
description: "POST body for creating a push subscription",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
subscription: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
endpoint: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Endpoint URL that is called when a notification event occurs."
|
||||||
|
},
|
||||||
|
keys: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
p256dh: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
|
||||||
|
},
|
||||||
|
auth: %Schema{
|
||||||
|
type: :string,
|
||||||
|
description: "Auth secret. Base64 encoded string of 16 bytes of random data."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:p256dh, :auth]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:endpoint, :keys]
|
||||||
|
},
|
||||||
|
data: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
alerts: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||||
|
favourite: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Receive favourite notifications?"
|
||||||
|
},
|
||||||
|
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
|
||||||
|
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
|
||||||
|
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: [:subscription],
|
||||||
|
example: %{
|
||||||
|
"subscription" => %{
|
||||||
|
"endpoint" => "https://example.com/example/1234",
|
||||||
|
"keys" => %{
|
||||||
|
"auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
|
||||||
|
"p256dh" =>
|
||||||
|
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data" => %{
|
||||||
|
"alerts" => %{
|
||||||
|
"follow" => true,
|
||||||
|
"mention" => true,
|
||||||
|
"poll" => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "SubscriptionUpdateRequest",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
data: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
alerts: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
|
||||||
|
favourite: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Receive favourite notifications?"
|
||||||
|
},
|
||||||
|
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
|
||||||
|
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
|
||||||
|
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"data" => %{
|
||||||
|
"alerts" => %{
|
||||||
|
"follow" => true,
|
||||||
|
"favourite" => true,
|
||||||
|
"reblog" => true,
|
||||||
|
"mention" => true,
|
||||||
|
"poll" => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,6 +17,9 @@ def init(opts), do: opts
|
||||||
def call(conn, errors) do
|
def call(conn, errors) do
|
||||||
errors =
|
errors =
|
||||||
Enum.map(errors, fn
|
Enum.map(errors, fn
|
||||||
|
%{name: nil, reason: :invalid_enum} = err ->
|
||||||
|
%OpenApiSpex.Cast.Error{err | name: err.value}
|
||||||
|
|
||||||
%{name: nil} = err ->
|
%{name: nil} = err ->
|
||||||
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "Attachment",
|
||||||
|
description: "Represents a file or media attachment that can be added to a status.",
|
||||||
|
type: :object,
|
||||||
|
requried: [:id, :url, :preview_url],
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "The location of the original full-size attachment"
|
||||||
|
},
|
||||||
|
remote_url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description:
|
||||||
|
"The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
preview_url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "The location of a scaled-down preview of the attachment"
|
||||||
|
},
|
||||||
|
text_url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "A shorter URL for the attachment"
|
||||||
|
},
|
||||||
|
description: %Schema{
|
||||||
|
type: :string,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
|
||||||
|
},
|
||||||
|
type: %Schema{
|
||||||
|
type: :string,
|
||||||
|
enum: ["image", "video", "audio", "unknown"],
|
||||||
|
description: "The type of the attachment"
|
||||||
|
},
|
||||||
|
pleroma: %Schema{
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
mime_type: %Schema{type: :string, description: "mime type of the attachment"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
id: "1638338801",
|
||||||
|
type: "image",
|
||||||
|
url: "someurl",
|
||||||
|
remote_url: "someurl",
|
||||||
|
preview_url: "someurl",
|
||||||
|
text_url: "someurl",
|
||||||
|
description: nil,
|
||||||
|
pleroma: %{mime_type: "image/png"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.Conversation do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "Conversation",
|
||||||
|
description: "Represents a conversation with \"direct message\" visibility.",
|
||||||
|
type: :object,
|
||||||
|
required: [:id, :accounts, :unread],
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
accounts: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Account,
|
||||||
|
description: "Participants in the conversation"
|
||||||
|
},
|
||||||
|
unread: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Is the conversation currently marked as unread?"
|
||||||
|
},
|
||||||
|
# last_status: Status
|
||||||
|
last_status: %Schema{
|
||||||
|
allOf: [Status],
|
||||||
|
description: "The last status in the conversation, to be used for optional display"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "418450",
|
||||||
|
"unread" => true,
|
||||||
|
"accounts" => [Account.schema().example],
|
||||||
|
"last_status" => Status.schema().example
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.List do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "List",
|
||||||
|
description: "Represents a list of users",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string, description: "The internal database ID of the list"},
|
||||||
|
title: %Schema{type: :string, description: "The user-defined title of the list"}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "12249",
|
||||||
|
"title" => "Friends"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -11,26 +11,72 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
||||||
|
|
||||||
OpenApiSpex.schema(%{
|
OpenApiSpex.schema(%{
|
||||||
title: "Poll",
|
title: "Poll",
|
||||||
description: "Response schema for account custom fields",
|
description: "Represents a poll attached to a status",
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
id: FlakeID,
|
id: FlakeID,
|
||||||
expires_at: %Schema{type: :string, format: "date-time"},
|
expires_at: %Schema{
|
||||||
expired: %Schema{type: :boolean},
|
type: :string,
|
||||||
multiple: %Schema{type: :boolean},
|
format: :"date-time",
|
||||||
votes_count: %Schema{type: :integer},
|
nullable: true,
|
||||||
voted: %Schema{type: :boolean},
|
description: "When the poll ends"
|
||||||
emojis: %Schema{type: :array, items: Emoji},
|
},
|
||||||
|
expired: %Schema{type: :boolean, description: "Is the poll currently expired?"},
|
||||||
|
multiple: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Does the poll allow multiple-choice answers?"
|
||||||
|
},
|
||||||
|
votes_count: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
nullable: true,
|
||||||
|
description: "How many votes have been received. Number, or null if `multiple` is false."
|
||||||
|
},
|
||||||
|
voted: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"When called with a user token, has the authorized user voted? Boolean, or null if no current user."
|
||||||
|
},
|
||||||
|
emojis: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Emoji,
|
||||||
|
description: "Custom emoji to be used for rendering poll options."
|
||||||
|
},
|
||||||
options: %Schema{
|
options: %Schema{
|
||||||
type: :array,
|
type: :array,
|
||||||
items: %Schema{
|
items: %Schema{
|
||||||
|
title: "PollOption",
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
title: %Schema{type: :string},
|
title: %Schema{type: :string},
|
||||||
votes_count: %Schema{type: :integer}
|
votes_count: %Schema{type: :integer}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
description: "Possible answers for the poll."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
id: "34830",
|
||||||
|
expires_at: "2019-12-05T04:05:08.302Z",
|
||||||
|
expired: true,
|
||||||
|
multiple: false,
|
||||||
|
votes_count: 10,
|
||||||
|
voters_count: nil,
|
||||||
|
voted: true,
|
||||||
|
own_votes: [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
%{
|
||||||
|
title: "accept",
|
||||||
|
votes_count: 6
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
title: "deny",
|
||||||
|
votes_count: 4
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
emojis: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.PushSubscription do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "PushSubscription",
|
||||||
|
description: "Response schema for a push subscription",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{
|
||||||
|
anyOf: [%Schema{type: :string}, %Schema{type: :integer}],
|
||||||
|
description: "The id of the push subscription in the database."
|
||||||
|
},
|
||||||
|
endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."},
|
||||||
|
server_key: %Schema{type: :string, description: "The streaming server's VAPID key."},
|
||||||
|
alerts: %Schema{
|
||||||
|
type: :object,
|
||||||
|
description: "Which alerts should be delivered to the endpoint.",
|
||||||
|
properties: %{
|
||||||
|
follow: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Receive a push notification when someone has followed you?"
|
||||||
|
},
|
||||||
|
favourite: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Receive a push notification when a status you created has been favourited by someone else?"
|
||||||
|
},
|
||||||
|
reblog: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Receive a push notification when a status you created has been boosted by someone else?"
|
||||||
|
},
|
||||||
|
mention: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Receive a push notification when someone else has mentioned you in a status?"
|
||||||
|
},
|
||||||
|
poll: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Receive a push notification when a poll you voted in or created has ended? "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"id" => "328_183",
|
||||||
|
"endpoint" => "https://yourdomain.example/listener",
|
||||||
|
"alerts" => %{
|
||||||
|
"follow" => true,
|
||||||
|
"favourite" => true,
|
||||||
|
"reblog" => true,
|
||||||
|
"mention" => true,
|
||||||
|
"poll" => true
|
||||||
|
},
|
||||||
|
"server_key" =>
|
||||||
|
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "ScheduledStatus",
|
||||||
|
description: "Represents a status that will be published at a future scheduled date.",
|
||||||
|
type: :object,
|
||||||
|
required: [:id, :scheduled_at, :params],
|
||||||
|
properties: %{
|
||||||
|
id: %Schema{type: :string},
|
||||||
|
scheduled_at: %Schema{type: :string, format: :"date-time"},
|
||||||
|
media_attachments: %Schema{type: :array, items: Attachment},
|
||||||
|
params: %Schema{
|
||||||
|
type: :object,
|
||||||
|
required: [:text, :visibility],
|
||||||
|
properties: %{
|
||||||
|
text: %Schema{type: :string, nullable: true},
|
||||||
|
media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
|
||||||
|
sensitive: %Schema{type: :boolean, nullable: true},
|
||||||
|
spoiler_text: %Schema{type: :string, nullable: true},
|
||||||
|
visibility: %Schema{type: VisibilityScope, nullable: true},
|
||||||
|
scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||||
|
poll: %Schema{type: Poll, nullable: true},
|
||||||
|
in_reply_to_id: %Schema{type: :string, nullable: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
id: "3221",
|
||||||
|
scheduled_at: "2019-12-05T12:33:01.000Z",
|
||||||
|
params: %{
|
||||||
|
text: "test content",
|
||||||
|
media_ids: nil,
|
||||||
|
sensitive: nil,
|
||||||
|
spoiler_text: nil,
|
||||||
|
visibility: nil,
|
||||||
|
scheduled_at: nil,
|
||||||
|
poll: nil,
|
||||||
|
idempotency: nil,
|
||||||
|
in_reply_to_id: nil
|
||||||
|
},
|
||||||
|
media_attachments: [Attachment.schema().example]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -5,9 +5,11 @@
|
||||||
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
alias OpenApiSpex.Schema
|
alias OpenApiSpex.Schema
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Attachment
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
alias Pleroma.Web.ApiSpec.Schemas.Emoji
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
alias Pleroma.Web.ApiSpec.Schemas.Poll
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||||
|
|
||||||
require OpenApiSpex
|
require OpenApiSpex
|
||||||
|
@ -50,22 +52,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
language: %Schema{type: :string, nullable: true},
|
language: %Schema{type: :string, nullable: true},
|
||||||
media_attachments: %Schema{
|
media_attachments: %Schema{
|
||||||
type: :array,
|
type: :array,
|
||||||
items: %Schema{
|
items: Attachment
|
||||||
type: :object,
|
|
||||||
properties: %{
|
|
||||||
id: %Schema{type: :string},
|
|
||||||
url: %Schema{type: :string, format: :uri},
|
|
||||||
remote_url: %Schema{type: :string, format: :uri},
|
|
||||||
preview_url: %Schema{type: :string, format: :uri},
|
|
||||||
text_url: %Schema{type: :string, format: :uri},
|
|
||||||
description: %Schema{type: :string},
|
|
||||||
type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
|
|
||||||
pleroma: %Schema{
|
|
||||||
type: :object,
|
|
||||||
properties: %{mime_type: %Schema{type: :string}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mentions: %Schema{
|
mentions: %Schema{
|
||||||
type: :array,
|
type: :array,
|
||||||
|
@ -86,7 +73,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
properties: %{
|
properties: %{
|
||||||
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
|
content: %Schema{type: :object, additionalProperties: %Schema{type: :string}},
|
||||||
conversation_id: %Schema{type: :integer},
|
conversation_id: %Schema{type: :integer},
|
||||||
direct_conversation_id: %Schema{type: :string, nullable: true},
|
direct_conversation_id: %Schema{
|
||||||
|
type: :integer,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"The ID of the Mastodon direct message conversation the status is associated with (if any)"
|
||||||
|
},
|
||||||
emoji_reactions: %Schema{
|
emoji_reactions: %Schema{
|
||||||
type: :array,
|
type: :array,
|
||||||
items: %Schema{
|
items: %Schema{
|
||||||
|
@ -115,16 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
||||||
replies_count: %Schema{type: :integer},
|
replies_count: %Schema{type: :integer},
|
||||||
sensitive: %Schema{type: :boolean},
|
sensitive: %Schema{type: :boolean},
|
||||||
spoiler_text: %Schema{type: :string},
|
spoiler_text: %Schema{type: :string},
|
||||||
tags: %Schema{
|
tags: %Schema{type: :array, items: Tag},
|
||||||
type: :array,
|
|
||||||
items: %Schema{
|
|
||||||
type: :object,
|
|
||||||
properties: %{
|
|
||||||
name: %Schema{type: :string},
|
|
||||||
url: %Schema{type: :string, format: :uri}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uri: %Schema{type: :string, format: :uri},
|
uri: %Schema{type: :string, format: :uri},
|
||||||
url: %Schema{type: :string, nullable: true, format: :uri},
|
url: %Schema{type: :string, nullable: true, format: :uri},
|
||||||
visibility: VisibilityScope
|
visibility: VisibilityScope
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "Tag",
|
||||||
|
description: "Represents a hashtag used within the content of a status",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
|
||||||
|
url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "A link to the hashtag on the instance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
name: "cofe",
|
||||||
|
url: "https://lain.com/tag/cofe"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
|
@ -19,8 +19,8 @@ def get_user(%Plug.Conn{} = conn) do
|
||||||
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
|
{_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
else
|
else
|
||||||
error ->
|
{:error, _reason} = error -> error
|
||||||
{:error, error}
|
error -> {:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.Auth.TOTPAuthenticator do
|
||||||
|
alias Comeonin.Pbkdf2
|
||||||
|
alias Pleroma.MFA
|
||||||
|
alias Pleroma.MFA.TOTP
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
@doc "Verify code or check backup code."
|
||||||
|
@spec verify(String.t(), User.t()) ::
|
||||||
|
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
|
||||||
|
def verify(
|
||||||
|
token,
|
||||||
|
%User{
|
||||||
|
multi_factor_authentication_settings:
|
||||||
|
%{enabled: true, totp: %{secret: secret, confirmed: true}} = _
|
||||||
|
} = _user
|
||||||
|
)
|
||||||
|
when is_binary(token) and byte_size(token) > 0 do
|
||||||
|
TOTP.validate_token(secret, token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify(_, _), do: {:error, :invalid_token}
|
||||||
|
|
||||||
|
@spec verify_recovery_code(User.t(), String.t()) ::
|
||||||
|
{:ok, :pass} | {:error, :invalid_token}
|
||||||
|
def verify_recovery_code(
|
||||||
|
%User{multi_factor_authentication_settings: %{enabled: true, backup_codes: codes}} = user,
|
||||||
|
code
|
||||||
|
)
|
||||||
|
when is_list(codes) and is_binary(code) do
|
||||||
|
hash_code = Enum.find(codes, fn hash -> Pbkdf2.checkpw(code, hash) end)
|
||||||
|
|
||||||
|
if hash_code do
|
||||||
|
MFA.invalidate_backup_code(user, hash_code)
|
||||||
|
{:ok, :pass}
|
||||||
|
else
|
||||||
|
{:error, :invalid_token}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_recovery_code(_, _), do: {:error, :invalid_token}
|
||||||
|
end
|
|
@ -24,6 +24,14 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
def unblock(blocker, blocked) do
|
||||||
|
with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
|
||||||
|
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
|
||||||
|
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
|
||||||
|
{:ok, unblock}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def follow(follower, followed) do
|
def follow(follower, followed) do
|
||||||
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
|
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
|
||||||
|
|
||||||
|
@ -43,8 +51,8 @@ def unfollow(follower, unfollowed) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def accept_follow_request(follower, followed) do
|
def accept_follow_request(follower, followed) do
|
||||||
with {:ok, follower} <- User.follow(follower, followed),
|
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||||
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
{:ok, follower} <- User.follow(follower, followed),
|
||||||
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
||||||
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
|
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
|
||||||
{:ok, _activity} <-
|
{:ok, _activity} <-
|
||||||
|
@ -79,8 +87,8 @@ def delete(activity_id, user) do
|
||||||
{:find_activity, Activity.get_by_id_with_object(activity_id)},
|
{:find_activity, Activity.get_by_id_with_object(activity_id)},
|
||||||
%Object{} = object <- Object.normalize(activity),
|
%Object{} = object <- Object.normalize(activity),
|
||||||
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
||||||
{:ok, _} <- unpin(activity_id, user),
|
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
||||||
{:ok, delete} <- ActivityPub.delete(object) do
|
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
||||||
{:ok, delete}
|
{:ok, delete}
|
||||||
else
|
else
|
||||||
{:find_activity, _} -> {:error, :not_found}
|
{:find_activity, _} -> {:error, :not_found}
|
||||||
|
@ -107,9 +115,12 @@ def repeat(id, user, params \\ %{}) do
|
||||||
|
|
||||||
def unrepeat(id, user) do
|
def unrepeat(id, user) do
|
||||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||||
{:find_activity, Activity.get_by_id(id)} do
|
{:find_activity, Activity.get_by_id(id)},
|
||||||
object = Object.normalize(activity)
|
%Object{} = note <- Object.normalize(activity, false),
|
||||||
ActivityPub.unannounce(user, object)
|
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
|
||||||
|
{:ok, undo, _} <- Builder.undo(user, announce),
|
||||||
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||||
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
{:find_activity, _} -> {:error, :not_found}
|
{:find_activity, _} -> {:error, :not_found}
|
||||||
_ -> {:error, dgettext("errors", "Could not unrepeat")}
|
_ -> {:error, dgettext("errors", "Could not unrepeat")}
|
||||||
|
@ -166,9 +177,12 @@ def favorite_helper(user, id) do
|
||||||
|
|
||||||
def unfavorite(id, user) do
|
def unfavorite(id, user) do
|
||||||
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
||||||
{:find_activity, Activity.get_by_id(id)} do
|
{:find_activity, Activity.get_by_id(id)},
|
||||||
object = Object.normalize(activity)
|
%Object{} = note <- Object.normalize(activity, false),
|
||||||
ActivityPub.unlike(user, object)
|
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
|
||||||
|
{:ok, undo, _} <- Builder.undo(user, like),
|
||||||
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||||
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
{:find_activity, _} -> {:error, :not_found}
|
{:find_activity, _} -> {:error, :not_found}
|
||||||
_ -> {:error, dgettext("errors", "Could not unfavorite")}
|
_ -> {:error, dgettext("errors", "Could not unfavorite")}
|
||||||
|
@ -177,8 +191,10 @@ def unfavorite(id, user) do
|
||||||
|
|
||||||
def react_with_emoji(id, user, emoji) do
|
def react_with_emoji(id, user, emoji) do
|
||||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||||
object <- Object.normalize(activity) do
|
object <- Object.normalize(activity),
|
||||||
ActivityPub.react_with_emoji(user, object, emoji)
|
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
|
||||||
|
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
|
||||||
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
{:error, dgettext("errors", "Could not add reaction emoji")}
|
{:error, dgettext("errors", "Could not add reaction emoji")}
|
||||||
|
@ -186,8 +202,10 @@ def react_with_emoji(id, user, emoji) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def unreact_with_emoji(id, user, emoji) do
|
def unreact_with_emoji(id, user, emoji) do
|
||||||
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
|
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
|
||||||
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
|
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
|
||||||
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
||||||
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
{:error, dgettext("errors", "Could not remove reaction emoji")}
|
{:error, dgettext("errors", "Could not remove reaction emoji")}
|
||||||
|
|
|
@ -402,6 +402,7 @@ defp shortname(name) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||||
def confirm_current_password(user, password) do
|
def confirm_current_password(user, password) do
|
||||||
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
|
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
|
||||||
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
|
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
defmodule Pleroma.Web.Endpoint do
|
defmodule Pleroma.Web.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :pleroma
|
use Phoenix.Endpoint, otp_app: :pleroma
|
||||||
|
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
socket("/socket", Pleroma.Web.UserSocket)
|
socket("/socket", Pleroma.Web.UserSocket)
|
||||||
|
|
||||||
plug(Pleroma.Plugs.SetLocalePlug)
|
plug(Pleroma.Plugs.SetLocalePlug)
|
||||||
|
@ -34,8 +36,7 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
Plug.Static,
|
Plug.Static,
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :pleroma,
|
from: :pleroma,
|
||||||
only:
|
only: Pleroma.Constants.static_only_files(),
|
||||||
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
|
|
||||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||||
gzip: true,
|
gzip: true,
|
||||||
cache_control_for_etags: @static_cache_control,
|
cache_control_for_etags: @static_cache_control,
|
||||||
|
|
|
@ -27,7 +27,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params)
|
||||||
when format in ["json", "activity+json"] do
|
when format in ["json", "activity+json"] do
|
||||||
with %{halted: false} = conn <-
|
with %{halted: false} = conn <-
|
||||||
Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
|
Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn,
|
||||||
unless_func: &Pleroma.Web.FederatingPlug.federating?/0
|
unless_func: &Pleroma.Web.FederatingPlug.federating?/1
|
||||||
) do
|
) do
|
||||||
ActivityPubController.call(conn, :user)
|
ActivityPubController.call(conn, :user)
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
|
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
|
||||||
|
|
||||||
|
@ -356,8 +356,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||||
|
|
||||||
@doc "POST /api/v1/accounts/:id/unblock"
|
@doc "POST /api/v1/accounts/:id/unblock"
|
||||||
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||||
with {:ok, _user_block} <- User.unblock(blocker, blocked),
|
with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
|
||||||
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
|
|
||||||
render(conn, "relationship.json", user: blocker, target: blocked)
|
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||||
else
|
else
|
||||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
@local_mastodon_name "Mastodon-Local"
|
@local_mastodon_name "Mastodon-Local"
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,12 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
|
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
|
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/conversations"
|
@doc "GET /api/v1/conversations"
|
||||||
def index(%{assigns: %{user: user}} = conn, params) do
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
participations = Participation.for_user_with_last_activity_id(user, params)
|
participations = Participation.for_user_with_last_activity_id(user, params)
|
||||||
|
@ -26,7 +29,7 @@ def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/conversations/:id/read"
|
@doc "POST /api/v1/conversations/:id/read"
|
||||||
def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
|
def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
|
||||||
with %Participation{} = participation <-
|
with %Participation{} = participation <-
|
||||||
Repo.get_by(Participation, id: participation_id, user_id: user.id),
|
Repo.get_by(Participation, id: participation_id, user_id: user.id),
|
||||||
{:ok, participation} <- Participation.mark_as_read(participation) do
|
{:ok, participation} <- Participation.mark_as_read(participation) do
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
|
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
:skip_plug,
|
:skip_plug,
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
@oauth_read_actions [:show, :index]
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
|
plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -17,60 +18,60 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do
|
||||||
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
|
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/filters"
|
@doc "GET /api/v1/filters"
|
||||||
def index(%{assigns: %{user: user}} = conn, _) do
|
def index(%{assigns: %{user: user}} = conn, _) do
|
||||||
filters = Filter.get_filters(user)
|
filters = Filter.get_filters(user)
|
||||||
|
|
||||||
render(conn, "filters.json", filters: filters)
|
render(conn, "index.json", filters: filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/filters"
|
@doc "POST /api/v1/filters"
|
||||||
def create(
|
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||||
%{assigns: %{user: user}} = conn,
|
|
||||||
%{"phrase" => phrase, "context" => context} = params
|
|
||||||
) do
|
|
||||||
query = %Filter{
|
query = %Filter{
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
phrase: phrase,
|
phrase: params.phrase,
|
||||||
context: context,
|
context: params.context,
|
||||||
hide: Map.get(params, "irreversible", false),
|
hide: params.irreversible,
|
||||||
whole_word: Map.get(params, "boolean", true)
|
whole_word: params.whole_word
|
||||||
# expires_at
|
# TODO: support `expires_in` parameter (as in Mastodon API)
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, response} = Filter.create(query)
|
{:ok, response} = Filter.create(query)
|
||||||
|
|
||||||
render(conn, "filter.json", filter: response)
|
render(conn, "show.json", filter: response)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "GET /api/v1/filters/:id"
|
@doc "GET /api/v1/filters/:id"
|
||||||
def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
|
def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
|
||||||
filter = Filter.get(filter_id, user)
|
filter = Filter.get(filter_id, user)
|
||||||
|
|
||||||
render(conn, "filter.json", filter: filter)
|
render(conn, "show.json", filter: filter)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "PUT /api/v1/filters/:id"
|
@doc "PUT /api/v1/filters/:id"
|
||||||
def update(
|
def update(
|
||||||
%{assigns: %{user: user}} = conn,
|
%{assigns: %{user: user}, body_params: params} = conn,
|
||||||
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
|
%{id: filter_id}
|
||||||
) do
|
) do
|
||||||
query = %Filter{
|
params =
|
||||||
user_id: user.id,
|
params
|
||||||
filter_id: filter_id,
|
|> Map.delete(:irreversible)
|
||||||
phrase: phrase,
|
|> Map.put(:hide, params[:irreversible])
|
||||||
context: context,
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
hide: Map.get(params, "irreversible", nil),
|
|> Map.new()
|
||||||
whole_word: Map.get(params, "boolean", true)
|
|
||||||
# expires_at
|
|
||||||
}
|
|
||||||
|
|
||||||
{:ok, response} = Filter.update(query)
|
# TODO: support `expires_in` parameter (as in Mastodon API)
|
||||||
render(conn, "filter.json", filter: response)
|
|
||||||
|
with %Filter{} = filter <- Filter.get(filter_id, user),
|
||||||
|
{:ok, %Filter{} = filter} <- Filter.update(filter, params) do
|
||||||
|
render(conn, "show.json", filter: filter)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "DELETE /api/v1/filters/:id"
|
@doc "DELETE /api/v1/filters/:id"
|
||||||
def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
|
def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do
|
||||||
query = %Filter{
|
query = %Filter{
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
filter_id: filter_id
|
filter_id: filter_id
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(:assign_follower when action != :index)
|
plug(:assign_follower when action != :index)
|
||||||
|
|
||||||
action_fallback(:errors)
|
action_fallback(:errors)
|
||||||
|
@ -21,6 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
|
||||||
%{scopes: ["follow", "write:follows"]} when action != :index
|
%{scopes: ["follow", "write:follows"]} when action != :index
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/follow_requests"
|
@doc "GET /api/v1/follow_requests"
|
||||||
def index(%{assigns: %{user: followed}} = conn, _params) do
|
def index(%{assigns: %{user: followed}} = conn, _params) do
|
||||||
follow_requests = User.get_follow_requests(followed)
|
follow_requests = User.get_follow_requests(followed)
|
||||||
|
@ -42,7 +45,7 @@ def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_follower(%{params: %{"id" => id}} = conn, _) do
|
defp assign_follower(%{params: %{id: id}} = conn, _) do
|
||||||
case User.get_cached_by_id(id) do
|
case User.get_cached_by_id(id) do
|
||||||
%User{} = follower -> assign(conn, :follower, follower)
|
%User{} = follower -> assign(conn, :follower, follower)
|
||||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||||
|
|
|
@ -5,12 +5,16 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
plug(OpenApiSpex.Plug.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
:skip_plug,
|
:skip_plug,
|
||||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||||
when action in [:show, :peers]
|
when action in [:show, :peers]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/instance"
|
@doc "GET /api/v1/instance"
|
||||||
def show(conn, _params) do
|
def show(conn, _params) do
|
||||||
render(conn, "show.json")
|
render(conn, "show.json")
|
||||||
|
|
|
@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
|
|
||||||
plug(:list_by_id_and_user when action not in [:index, :create])
|
|
||||||
|
|
||||||
@oauth_read_actions [:index, :show, :list_accounts]
|
@oauth_read_actions [:index, :show, :list_accounts]
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
plug(:list_by_id_and_user when action not in [:index, :create])
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
|
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
|
||||||
plug(
|
|
||||||
OAuthScopesPlug,
|
|
||||||
%{scopes: ["write:lists"]}
|
|
||||||
when action not in @oauth_read_actions
|
|
||||||
)
|
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
|
||||||
|
|
||||||
# GET /api/v1/lists
|
# GET /api/v1/lists
|
||||||
def index(%{assigns: %{user: user}} = conn, opts) do
|
def index(%{assigns: %{user: user}} = conn, opts) do
|
||||||
lists = Pleroma.List.for_user(user, opts)
|
lists = Pleroma.List.for_user(user, opts)
|
||||||
|
@ -30,7 +27,7 @@ def index(%{assigns: %{user: user}} = conn, opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /api/v1/lists
|
# POST /api/v1/lists
|
||||||
def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
|
def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
|
||||||
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
|
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
|
||||||
render(conn, "show.json", list: list)
|
render(conn, "show.json", list: list)
|
||||||
end
|
end
|
||||||
|
@ -42,7 +39,7 @@ def show(%{assigns: %{list: list}} = conn, _) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# PUT /api/v1/lists/:id
|
# PUT /api/v1/lists/:id
|
||||||
def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
|
def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
|
||||||
with {:ok, list} <- Pleroma.List.rename(list, title) do
|
with {:ok, list} <- Pleroma.List.rename(list, title) do
|
||||||
render(conn, "show.json", list: list)
|
render(conn, "show.json", list: list)
|
||||||
end
|
end
|
||||||
|
@ -65,7 +62,7 @@ def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /api/v1/lists/:id/accounts
|
# POST /api/v1/lists/:id/accounts
|
||||||
def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
|
def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
|
||||||
Enum.each(account_ids, fn account_id ->
|
Enum.each(account_ids, fn account_id ->
|
||||||
with %User{} = followed <- User.get_cached_by_id(account_id) do
|
with %User{} = followed <- User.get_cached_by_id(account_id) do
|
||||||
Pleroma.List.follow(list, followed)
|
Pleroma.List.follow(list, followed)
|
||||||
|
@ -76,7 +73,10 @@ def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
# DELETE /api/v1/lists/:id/accounts
|
# DELETE /api/v1/lists/:id/accounts
|
||||||
def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
|
def remove_from_list(
|
||||||
|
%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn,
|
||||||
|
_
|
||||||
|
) do
|
||||||
Enum.each(account_ids, fn account_id ->
|
Enum.each(account_ids, fn account_id ->
|
||||||
with %User{} = followed <- User.get_cached_by_id(account_id) do
|
with %User{} = followed <- User.get_cached_by_id(account_id) do
|
||||||
Pleroma.List.unfollow(list, followed)
|
Pleroma.List.unfollow(list, followed)
|
||||||
|
@ -86,7 +86,7 @@ def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => accoun
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
|
defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
|
||||||
case Pleroma.List.get(id, user) do
|
case Pleroma.List.get(id, user) do
|
||||||
%Pleroma.List{} = list -> assign(conn, :list, list)
|
%Pleroma.List{} = list -> assign(conn, :list, list)
|
||||||
nil -> conn |> render_error(:not_found, "List not found") |> halt()
|
nil -> conn |> render_error(:not_found, "List not found") |> halt()
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read:statuses"]}
|
%{scopes: ["read:statuses"]}
|
||||||
|
@ -16,14 +18,18 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation
|
||||||
|
|
||||||
# GET /api/v1/markers
|
# GET /api/v1/markers
|
||||||
def index(%{assigns: %{user: user}} = conn, params) do
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
markers = Pleroma.Marker.get_markers(user, params["timeline"])
|
markers = Pleroma.Marker.get_markers(user, params[:timeline])
|
||||||
render(conn, "markers.json", %{markers: markers})
|
render(conn, "markers.json", %{markers: markers})
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /api/v1/markers
|
# POST /api/v1/markers
|
||||||
def upsert(%{assigns: %{user: user}} = conn, params) do
|
def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||||
|
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
|
||||||
|
|
||||||
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
|
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
|
||||||
markers <- Map.values(result) do
|
markers <- Map.values(result) do
|
||||||
render(conn, "markers.json", %{markers: markers})
|
render(conn, "markers.json", %{markers: markers})
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
@oauth_read_actions [:show, :index]
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
|
|
|
@ -15,6 +15,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
|
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :show
|
||||||
|
@ -22,8 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
|
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :vote)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/polls/:id"
|
@doc "GET /api/v1/polls/:id"
|
||||||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
true <- Visibility.visible_for_user?(activity, user) do
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
|
@ -35,7 +39,7 @@ def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/polls/:id/votes"
|
@doc "POST /api/v1/polls/:id/votes"
|
||||||
def vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
|
def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
|
||||||
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
|
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||||
true <- Visibility.visible_for_user?(activity, user),
|
true <- Visibility.visible_for_user?(activity, user),
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
|
||||||
|
|
||||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
|
||||||
|
|
|
@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
|
||||||
alias Pleroma.ScheduledActivity
|
alias Pleroma.ScheduledActivity
|
||||||
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||||
|
|
||||||
plug(:assign_scheduled_activity when action != :index)
|
|
||||||
|
|
||||||
@oauth_read_actions [:show, :index]
|
@oauth_read_actions [:show, :index]
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
|
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
|
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
|
||||||
|
plug(:assign_scheduled_activity when action != :index)
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
|
||||||
|
|
||||||
@doc "GET /api/v1/scheduled_statuses"
|
@doc "GET /api/v1/scheduled_statuses"
|
||||||
def index(%{assigns: %{user: user}} = conn, params) do
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
|
||||||
|
|
||||||
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
|
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
|
||||||
conn
|
conn
|
||||||
|> add_link_headers(scheduled_activities)
|
|> add_link_headers(scheduled_activities)
|
||||||
|
@ -35,7 +39,7 @@ def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "PUT /api/v1/scheduled_statuses/:id"
|
@doc "PUT /api/v1/scheduled_statuses/:id"
|
||||||
def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
|
def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
|
||||||
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
|
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
|
||||||
render(conn, "show.json", scheduled_activity: scheduled_activity)
|
render(conn, "show.json", scheduled_activity: scheduled_activity)
|
||||||
end
|
end
|
||||||
|
@ -48,7 +52,7 @@ def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
|
defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
|
||||||
case ScheduledActivity.get(user, id) do
|
case ScheduledActivity.get(user, id) do
|
||||||
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
|
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
|
||||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.SearchController do
|
defmodule Pleroma.Web.MastodonAPI.SearchController do
|
||||||
use Pleroma.Web, :controller
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1]
|
import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1]
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Plugs.OAuthScopesPlug
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
@ -18,6 +18,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
|
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
|
||||||
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
|
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
|
||||||
|
|
||||||
|
@ -25,7 +27,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
||||||
|
|
||||||
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
|
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
|
||||||
|
|
||||||
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
|
||||||
|
|
||||||
|
def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
|
||||||
accounts = User.search(query, search_options(params, user))
|
accounts = User.search(query, search_options(params, user))
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
@ -36,7 +40,7 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
|
||||||
def search2(conn, params), do: do_search(:v2, conn, params)
|
def search2(conn, params), do: do_search(:v2, conn, params)
|
||||||
def search(conn, params), do: do_search(:v1, conn, params)
|
def search(conn, params), do: do_search(:v1, conn, params)
|
||||||
|
|
||||||
defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
|
||||||
options = search_options(params, user)
|
options = search_options(params, user)
|
||||||
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
|
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
|
||||||
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
|
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
|
||||||
|
@ -44,7 +48,7 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
|
||||||
result =
|
result =
|
||||||
default_values
|
default_values
|
||||||
|> Enum.map(fn {resource, default_value} ->
|
|> Enum.map(fn {resource, default_value} ->
|
||||||
if params["type"] in [nil, resource] do
|
if params[:type] in [nil, resource] do
|
||||||
{resource, fn -> resource_search(version, resource, query, options) end}
|
{resource, fn -> resource_search(version, resource, query, options) end}
|
||||||
else
|
else
|
||||||
{resource, fn -> default_value end}
|
{resource, fn -> default_value end}
|
||||||
|
@ -68,11 +72,11 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
|
||||||
defp search_options(params, user) do
|
defp search_options(params, user) do
|
||||||
[
|
[
|
||||||
skip_relationships: skip_relationships?(params),
|
skip_relationships: skip_relationships?(params),
|
||||||
resolve: params["resolve"] == "true",
|
resolve: params[:resolve],
|
||||||
following: params["following"] == "true",
|
following: params[:following],
|
||||||
limit: fetch_integer_param(params, "limit"),
|
limit: params[:limit],
|
||||||
offset: fetch_integer_param(params, "offset"),
|
offset: params[:offset],
|
||||||
type: params["type"],
|
type: params[:type],
|
||||||
author: get_author(params),
|
author: get_author(params),
|
||||||
for_user: user
|
for_user: user
|
||||||
]
|
]
|
||||||
|
@ -135,7 +139,7 @@ defp with_fallback(f, fallback \\ []) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_author(%{"account_id" => account_id}) when is_binary(account_id),
|
defp get_author(%{account_id: account_id}) when is_binary(account_id),
|
||||||
do: User.get_cached_by_id(account_id)
|
do: User.get_cached_by_id(account_id)
|
||||||
|
|
||||||
defp get_author(_params), do: nil
|
defp get_author(_params), do: nil
|
||||||
|
|
|
@ -206,9 +206,9 @@ def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/statuses/:id/unreblog"
|
@doc "POST /api/v1/statuses/:id/unreblog"
|
||||||
def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
def unreblog(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
|
||||||
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
|
with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
|
%Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||||
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
|
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -222,9 +222,9 @@ def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/statuses/:id/unfavourite"
|
@doc "POST /api/v1/statuses/:id/unfavourite"
|
||||||
def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
|
def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
|
||||||
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
|
with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
|
||||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
|
%Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,14 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
||||||
|
|
||||||
action_fallback(:errors)
|
action_fallback(:errors)
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
plug(:restrict_push_enabled)
|
||||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
|
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
|
||||||
|
|
||||||
plug(:restrict_push_enabled)
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
|
||||||
|
|
||||||
# Creates PushSubscription
|
# Creates PushSubscription
|
||||||
# POST /api/v1/push/subscription
|
# POST /api/v1/push/subscription
|
||||||
#
|
#
|
||||||
def create(%{assigns: %{user: user, token: token}} = conn, params) do
|
def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
|
||||||
with {:ok, _} <- Subscription.delete_if_exists(user, token),
|
with {:ok, _} <- Subscription.delete_if_exists(user, token),
|
||||||
{:ok, subscription} <- Subscription.create(user, token, params) do
|
{:ok, subscription} <- Subscription.create(user, token, params) do
|
||||||
render(conn, "show.json", subscription: subscription)
|
render(conn, "show.json", subscription: subscription)
|
||||||
|
@ -28,7 +30,7 @@ def create(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||||
# Gets PushSubscription
|
# Gets PushSubscription
|
||||||
# GET /api/v1/push/subscription
|
# GET /api/v1/push/subscription
|
||||||
#
|
#
|
||||||
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
|
def show(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||||
with {:ok, subscription} <- Subscription.get(user, token) do
|
with {:ok, subscription} <- Subscription.get(user, token) do
|
||||||
render(conn, "show.json", subscription: subscription)
|
render(conn, "show.json", subscription: subscription)
|
||||||
end
|
end
|
||||||
|
@ -37,7 +39,7 @@ def get(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||||
# Updates PushSubscription
|
# Updates PushSubscription
|
||||||
# PUT /api/v1/push/subscription
|
# PUT /api/v1/push/subscription
|
||||||
#
|
#
|
||||||
def update(%{assigns: %{user: user, token: token}} = conn, params) do
|
def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
|
||||||
with {:ok, subscription} <- Subscription.update(user, token, params) do
|
with {:ok, subscription} <- Subscription.update(user, token, params) do
|
||||||
render(conn, "show.json", subscription: subscription)
|
render(conn, "show.json", subscription: subscription)
|
||||||
end
|
end
|
||||||
|
@ -66,7 +68,7 @@ defp restrict_push_enabled(conn, _) do
|
||||||
def errors(conn, {:error, :not_found}) do
|
def errors(conn, {:error, :not_found}) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:not_found)
|
|> put_status(:not_found)
|
||||||
|> json(dgettext("errors", "Not found"))
|
|> json(%{error: dgettext("errors", "Record not found")})
|
||||||
end
|
end
|
||||||
|
|
||||||
def errors(conn, _) do
|
def errors(conn, _) do
|
||||||
|
|
|
@ -37,9 +37,11 @@ def render("index.json", %{users: users} = opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("show.json", %{user: user} = opts) do
|
def render("show.json", %{user: user} = opts) do
|
||||||
if User.visible_for?(user, opts[:for]),
|
if User.visible_for?(user, opts[:for]) do
|
||||||
do: do_render("show.json", opts),
|
do_render("show.json", opts)
|
||||||
else: %{}
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("mention.json", %{user: user}) do
|
def render("mention.json", %{user: user}) do
|
||||||
|
@ -224,7 +226,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
fields: user.fields,
|
fields: user.fields,
|
||||||
bot: bot,
|
bot: bot,
|
||||||
source: %{
|
source: %{
|
||||||
note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
|
note: prepare_user_bio(user),
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: user.raw_fields,
|
fields: user.raw_fields,
|
||||||
pleroma: %{
|
pleroma: %{
|
||||||
|
@ -256,8 +258,17 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
|> maybe_put_follow_requests_count(user, opts[:for])
|
|> maybe_put_follow_requests_count(user, opts[:for])
|
||||||
|> maybe_put_allow_following_move(user, opts[:for])
|
|> maybe_put_allow_following_move(user, opts[:for])
|
||||||
|> maybe_put_unread_conversation_count(user, opts[:for])
|
|> maybe_put_unread_conversation_count(user, opts[:for])
|
||||||
|
|> maybe_put_unread_notification_count(user, opts[:for])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp prepare_user_bio(%User{bio: ""}), do: ""
|
||||||
|
|
||||||
|
defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
|
||||||
|
bio |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_user_bio(_), do: ""
|
||||||
|
|
||||||
defp username_from_nickname(string) when is_binary(string) do
|
defp username_from_nickname(string) when is_binary(string) do
|
||||||
hd(String.split(string, "@"))
|
hd(String.split(string, "@"))
|
||||||
end
|
end
|
||||||
|
@ -353,6 +364,16 @@ defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{
|
||||||
|
|
||||||
defp maybe_put_unread_conversation_count(data, _, _), do: data
|
defp maybe_put_unread_conversation_count(data, _, _), do: data
|
||||||
|
|
||||||
|
defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do
|
||||||
|
Kernel.put_in(
|
||||||
|
data,
|
||||||
|
[:pleroma, :unread_notifications_count],
|
||||||
|
Pleroma.Notification.unread_notifications_count(user)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_unread_notification_count(data, _, _), do: data
|
||||||
|
|
||||||
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
|
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
|
||||||
defp image_url(_), do: nil
|
defp image_url(_), do: nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.FilterView
|
alias Pleroma.Web.MastodonAPI.FilterView
|
||||||
|
|
||||||
def render("filters.json", %{filters: filters} = opts) do
|
def render("index.json", %{filters: filters}) do
|
||||||
render_many(filters, FilterView, "filter.json", opts)
|
render_many(filters, FilterView, "show.json")
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("filter.json", %{filter: filter}) do
|
def render("show.json", %{filter: filter}) do
|
||||||
expires_at =
|
expires_at =
|
||||||
if filter.expires_at do
|
if filter.expires_at do
|
||||||
Utils.to_masto_date(filter.expires_at)
|
Utils.to_masto_date(filter.expires_at)
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
||||||
use Pleroma.Web, :view
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.ActivityPub.MRF
|
||||||
|
|
||||||
@mastodon_api_level "2.7.2"
|
@mastodon_api_level "2.7.2"
|
||||||
|
|
||||||
def render("show.json", _) do
|
def render("show.json", _) do
|
||||||
instance = Pleroma.Config.get(:instance)
|
instance = Config.get(:instance)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
uri: Pleroma.Web.base_url(),
|
uri: Pleroma.Web.base_url(),
|
||||||
|
@ -29,7 +32,58 @@ def render("show.json", _) do
|
||||||
upload_limit: Keyword.get(instance, :upload_limit),
|
upload_limit: Keyword.get(instance, :upload_limit),
|
||||||
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
||||||
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
||||||
banner_upload_limit: Keyword.get(instance, :banner_upload_limit)
|
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
|
||||||
|
pleroma: %{
|
||||||
|
metadata: %{
|
||||||
|
features: features(),
|
||||||
|
federation: federation()
|
||||||
|
},
|
||||||
|
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def features do
|
||||||
|
[
|
||||||
|
"pleroma_api",
|
||||||
|
"mastodon_api",
|
||||||
|
"mastodon_api_streaming",
|
||||||
|
"polls",
|
||||||
|
"pleroma_explicit_addressing",
|
||||||
|
"shareable_emoji_packs",
|
||||||
|
"multifetch",
|
||||||
|
"pleroma:api/v1/notifications:include_types_filter",
|
||||||
|
if Config.get([:media_proxy, :enabled]) do
|
||||||
|
"media_proxy"
|
||||||
|
end,
|
||||||
|
if Config.get([:gopher, :enabled]) do
|
||||||
|
"gopher"
|
||||||
|
end,
|
||||||
|
if Config.get([:chat, :enabled]) do
|
||||||
|
"chat"
|
||||||
|
end,
|
||||||
|
if Config.get([:instance, :allow_relay]) do
|
||||||
|
"relay"
|
||||||
|
end,
|
||||||
|
if Config.get([:instance, :safe_dm_mentions]) do
|
||||||
|
"safe_dm_mentions"
|
||||||
|
end,
|
||||||
|
"pleroma_emoji_reactions"
|
||||||
|
]
|
||||||
|
|> Enum.filter(& &1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def federation do
|
||||||
|
quarantined = Config.get([:instance, :quarantined_instances], [])
|
||||||
|
|
||||||
|
if Config.get([:instance, :mrf_transparency]) do
|
||||||
|
{:ok, data} = MRF.describe()
|
||||||
|
|
||||||
|
data
|
||||||
|
|> Map.merge(%{quarantined_instances: quarantined})
|
||||||
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|> Map.put(:enabled, Config.get([:instance, :federating]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,12 +6,16 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do
|
||||||
use Pleroma.Web, :view
|
use Pleroma.Web, :view
|
||||||
|
|
||||||
def render("markers.json", %{markers: markers}) do
|
def render("markers.json", %{markers: markers}) do
|
||||||
Enum.reduce(markers, %{}, fn m, acc ->
|
Map.new(markers, fn m ->
|
||||||
Map.put_new(acc, m.timeline, %{
|
{m.timeline,
|
||||||
|
%{
|
||||||
last_read_id: m.last_read_id,
|
last_read_id: m.last_read_id,
|
||||||
version: m.lock_version,
|
version: m.lock_version,
|
||||||
updated_at: NaiveDateTime.to_iso8601(m.updated_at)
|
updated_at: NaiveDateTime.to_iso8601(m.updated_at),
|
||||||
})
|
pleroma: %{
|
||||||
|
unread_count: m.unread_count
|
||||||
|
}
|
||||||
|
}}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
||||||
|
|
||||||
@behaviour :cowboy_websocket
|
@behaviour :cowboy_websocket
|
||||||
|
|
||||||
|
# Cowboy timeout period.
|
||||||
|
@timeout :timer.seconds(30)
|
||||||
|
# Hibernate every X messages
|
||||||
|
@hibernate_every 100
|
||||||
|
|
||||||
@streams [
|
@streams [
|
||||||
"public",
|
"public",
|
||||||
"public:local",
|
"public:local",
|
||||||
|
@ -25,9 +30,6 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
||||||
]
|
]
|
||||||
@anonymous_streams ["public", "public:local", "hashtag"]
|
@anonymous_streams ["public", "public:local", "hashtag"]
|
||||||
|
|
||||||
# Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
|
|
||||||
@timeout :infinity
|
|
||||||
|
|
||||||
def init(%{qs: qs} = req, state) do
|
def init(%{qs: qs} = req, state) do
|
||||||
with params <- :cow_qs.parse_qs(qs),
|
with params <- :cow_qs.parse_qs(qs),
|
||||||
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
|
||||||
|
@ -42,7 +44,7 @@ def init(%{qs: qs} = req, state) do
|
||||||
req
|
req
|
||||||
end
|
end
|
||||||
|
|
||||||
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
|
{:cowboy_websocket, req, %{user: user, topic: topic, count: 0}, %{idle_timeout: @timeout}}
|
||||||
else
|
else
|
||||||
{:error, code} ->
|
{:error, code} ->
|
||||||
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
|
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
|
||||||
|
@ -57,7 +59,13 @@ def init(%{qs: qs} = req, state) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def websocket_init(state) do
|
def websocket_init(state) do
|
||||||
send(self(), :subscribe)
|
Logger.debug(
|
||||||
|
"#{__MODULE__} accepted websocket connection for user #{
|
||||||
|
(state.user || %{id: "anonymous"}).id
|
||||||
|
}, topic #{state.topic}"
|
||||||
|
)
|
||||||
|
|
||||||
|
Streamer.add_socket(state.topic, state.user)
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,19 +74,24 @@ def websocket_handle(_frame, state) do
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def websocket_info(:subscribe, state) do
|
def websocket_info({:render_with_user, view, template, item}, state) do
|
||||||
Logger.debug(
|
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
|
||||||
"#{__MODULE__} accepted websocket connection for user #{
|
|
||||||
(state.user || %{id: "anonymous"}).id
|
|
||||||
}, topic #{state.topic}"
|
|
||||||
)
|
|
||||||
|
|
||||||
Streamer.add_socket(state.topic, streamer_socket(state))
|
unless Streamer.filtered_by_user?(user, item) do
|
||||||
|
websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
|
||||||
|
else
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def websocket_info({:text, message}, state) do
|
def websocket_info({:text, message}, state) do
|
||||||
{:reply, {:text, message}, state}
|
# If the websocket processed X messages, force an hibernate/GC.
|
||||||
|
# We don't hibernate at every message to balance CPU usage/latency with RAM usage.
|
||||||
|
if state.count > @hibernate_every do
|
||||||
|
{:reply, {:text, message}, %{state | count: 0}, :hibernate}
|
||||||
|
else
|
||||||
|
{:reply, {:text, message}, %{state | count: state.count + 1}}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def terminate(reason, _req, state) do
|
def terminate(reason, _req, state) do
|
||||||
|
@ -88,7 +101,7 @@ def terminate(reason, _req, state) do
|
||||||
}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
Streamer.remove_socket(state.topic, streamer_socket(state))
|
Streamer.remove_socket(state.topic)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -136,8 +149,4 @@ defp expand_topic("list", params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp expand_topic(topic, _), do: topic
|
defp expand_topic(topic, _), do: topic
|
||||||
|
|
||||||
defp streamer_socket(state) do
|
|
||||||
%{transport_pid: self(), assigns: state}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue