This commit is contained in:
sadposter 2019-11-20 09:48:05 +00:00
commit 9d29a5063c
60 changed files with 2534 additions and 219 deletions

View file

@ -53,7 +53,7 @@ unit-testing:
- mix deps.get - mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --trace --preload-modules - mix coveralls --preload-modules
unit-testing-rum: unit-testing-rum:
stage: test stage: test
@ -68,7 +68,7 @@ unit-testing-rum:
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" - "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
- mix test --trace --preload-modules - mix test --preload-modules
lint: lint:
stage: test stage: test

View file

@ -25,10 +25,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body) - **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body)
- **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset - Admin API: Return link alongside with token on password reset
- Admin API: Support authentication via `x-admin-token` HTTP header
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) - Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
- 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
@ -41,10 +43,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app. - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- Job queue stats to the healthcheck page - Job queue stats to the healthcheck page
- Admin API: Add ability to fetch reports, grouped by status `GET /api/pleroma/admin/grouped_reports`
- Admin API: Add ability to require password reset - Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition) - Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items - Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
@ -53,14 +57,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity - Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/) - OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
- Metadata Link: Atom syndication Feed - Metadata Link: Atom syndication Feed
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints - Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array - Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body). - Admin API: Multiple endpoints now require `nicknames` array, instead of singe `nickname`:
- `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group`
- `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body)
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers - Mastodon API: Add `/api/v1/markers` for managing timeline read markers
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations` - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
- Configuration: `feed` option for user atom feed. - Configuration: `feed` option for user atom feed.
- Pleroma API: Add Emoji reactions
</details> </details>
### Fixed ### Fixed
@ -73,6 +81,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname` - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
</details> </details>
## [1.1.6] - 2019-11-19
### Fixed
- Not being able to log into to third party apps when the browser is logged into mastofe
- Email confirmation not being required even when enabled
- Mastodon API: conversations API crashing when one status is malformed
### Bundled Pleroma-FE Changes
#### Added
- About page
- Meme arrows
#### Fixed
- Image modal not closing unless clicked outside of image
- Attachment upload spinner not being centered
- Showing follow counters being 0 when they are actually hidden
## [1.1.5] - 2019-11-09
### Fixed
- Polls having different numbers in timelines/notifications/poll api endpoints due to cache desyncronization
- Pleroma API: OAuth token endpoint not being found when ".json" suffix is appended
### Changed
- Frontend bundle updated to [044c9ad0](https://git.pleroma.social/pleroma/pleroma-fe/commit/044c9ad0562af059dd961d50961a3880fca9c642)
## [1.1.4] - 2019-11-01
### Fixed
- Added a migration that fills up empty user.info fields to prevent breakage after previous unsafe migrations.
- Failure to migrate from pre-1.0.0 versions
- Mastodon API: Notification stream not including follow notifications
## [1.1.3] - 2019-10-25
### Fixed
- Blocked users showing up in notifications collapsed as if they were muted
- `pleroma_ctl` not working on Debian's default shell
## [1.1.2] - 2019-10-18 ## [1.1.2] - 2019-10-18
### Fixed ### Fixed
- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist. - `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.

View file

@ -381,6 +381,10 @@
accept: [], accept: [],
reject: [] reject: []
config :pleroma, :mrf_object_age,
threshold: 172_800,
actions: [:delist, :strip_followers]
config :pleroma, :rich_media, config :pleroma, :rich_media,
enabled: true, enabled: true,
ignore_hosts: [], ignore_hosts: [],

View file

@ -2,11 +2,10 @@
Authentication is required and the user must be an admin. Authentication is required and the user must be an admin.
## `/api/pleroma/admin/users` ## `GET /api/pleroma/admin/users`
### List users ### List users
- Method `GET`
- Query Params: - Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain) - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters: - *optional* `filters`: **string** comma-separated string of filters:
@ -51,7 +50,6 @@ Authentication is required and the user must be an admin.
### Remove a user ### Remove a user
- Method `DELETE`
- Params: - Params:
- `nickname` - `nickname`
- Response: Users nickname - Response: Users nickname
@ -60,7 +58,6 @@ Authentication is required and the user must be an admin.
### Remove a user ### Remove a user
- Method `DELETE`
- Params: - Params:
- `nicknames` - `nicknames`
- Response: Array of user nicknames - Response: Array of user nicknames
@ -78,31 +75,30 @@ Authentication is required and the user must be an admin.
] ]
- Response: Users nickname - Response: Users nickname
## `/api/pleroma/admin/users/follow` ## `POST /api/pleroma/admin/users/follow`
### Make a user follow another user ### Make a user follow another user
- Methods: `POST`
- Params: - Params:
- `follower`: The nickname of the follower - `follower`: The nickname of the follower
- `followed`: The nickname of the followed - `followed`: The nickname of the followed
- Response: - Response:
- "ok" - "ok"
## `POST /api/pleroma/admin/users/unfollow`
## `/api/pleroma/admin/users/unfollow`
### Make a user unfollow another user ### Make a user unfollow another user
- Methods: `POST`
- Params: - Params:
- `follower`: The nickname of the follower - `follower`: The nickname of the follower
- `followed`: The nickname of the followed - `followed`: The nickname of the followed
- Response: - Response:
- "ok" - "ok"
## `/api/pleroma/admin/users/:nickname/toggle_activation` ## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation ### Toggle user activation
- Method: `PATCH`
- Params: - Params:
- `nickname` - `nickname`
- Response: Users object - Response: Users object
@ -115,27 +111,26 @@ Authentication is required and the user must be an admin.
} }
``` ```
## `/api/pleroma/admin/users/tag` ## `PUT /api/pleroma/admin/users/tag`
### Tag a list of users ### Tag a list of users
- Method: `PUT`
- Params: - Params:
- `nicknames` (array) - `nicknames` (array)
- `tags` (array) - `tags` (array)
## `DELETE /api/pleroma/admin/users/tag`
### Untag a list of users ### Untag a list of users
- Method: `DELETE`
- Params: - Params:
- `nicknames` (array) - `nicknames` (array)
- `tags` (array) - `tags` (array)
## `/api/pleroma/admin/users/:nickname/permission_group` ## `GET /api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership ### Get user user permission groups membership
- Method: `GET`
- Params: none - Params: none
- Response: - Response:
@ -146,13 +141,12 @@ Authentication is required and the user must be an admin.
} }
``` ```
## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group` ## `GET /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist. Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist.
### Get user user permission groups membership per permission group ### Get user user permission groups membership per permission group
- Method: `GET`
- Params: none - Params: none
- Response: - Response:
@ -184,6 +178,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` ## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group ### Remove user from permission group
- Params: none - Params: none
@ -239,30 +235,20 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status` ## `GET /api/pleroma/admin/users/:nickname_or_id`
### Active or deactivate a user
- Params:
- `nickname`
- `status` BOOLEAN field, false value means deactivation.
## `/api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user ### Retrive the details of a user
- Method: `GET`
- Params: - Params:
- `nickname` or `id` - `nickname` or `id`
- Response: - Response:
- On failure: `Not found` - On failure: `Not found`
- On success: JSON of the user - On success: JSON of the user
## `/api/pleroma/admin/users/:nickname_or_id/statuses` ## `GET /api/pleroma/admin/users/:nickname_or_id/statuses`
### Retrive user's latest statuses ### Retrive user's latest statuses
- Method: `GET`
- Params: - Params:
- `nickname` or `id` - `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`) - *optional* `page_size`: number of statuses to return (default is `20`)
@ -271,19 +257,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On failure: `Not found` - On failure: `Not found`
- On success: JSON array of user's latest statuses - On success: JSON array of user's latest statuses
## `/api/pleroma/admin/relay` ## `POST /api/pleroma/admin/relay`
### Follow a Relay ### Follow a Relay
- Methods: `POST`
- Params: - Params:
- `relay_url` - `relay_url`
- Response: - Response:
- On success: URL of the followed relay - On success: URL of the followed relay
## `DELETE /api/pleroma/admin/relay`
### Unfollow a Relay ### Unfollow a Relay
- Methods: `DELETE`
- Params: - Params:
- `relay_url` - `relay_url`
- Response: - Response:
@ -297,11 +283,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response: - Response:
- On success: JSON array of relays - On success: JSON array of relays
## `/api/pleroma/admin/users/invite_token` ## `POST /api/pleroma/admin/users/invite_token`
### Create an account registration invite token ### Create an account registration invite token
- Methods: `POST`
- Params: - Params:
- *optional* `max_use` (integer) - *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07") - *optional* `expires_at` (date string e.g. "2019-04-07")
@ -319,11 +304,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/invites` ## `GET /api/pleroma/admin/users/invites`
### Get a list of generated invites ### Get a list of generated invites
- Methods: `GET`
- Params: none - Params: none
- Response: - Response:
@ -345,11 +329,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/revoke_invite` ## `POST /api/pleroma/admin/users/revoke_invite`
### Revoke invite by token ### Revoke invite by token
- Methods: `POST`
- Params: - Params:
- `token` - `token`
- Response: - Response:
@ -367,21 +350,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `POST /api/pleroma/admin/users/email_invite`
## `/api/pleroma/admin/users/email_invite`
### Sends registration invite via email ### Sends registration invite via email
- Methods: `POST`
- Params: - Params:
- `email` - `email`
- `name`, optional - `name`, optional
## `/api/pleroma/admin/users/:nickname/password_reset` ## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname
- Methods: `GET`
- Params: none - Params: none
- Response: - Response:
@ -392,18 +372,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/users/force_password_reset` ## `PATCH /api/pleroma/admin/users/force_password_reset`
### Force passord reset for a user with a given nickname ### Force passord reset for a user with a given nickname
- Methods: `PATCH`
- Params: - Params:
- `nicknames` - `nicknames`
- Response: none (code `204`) - Response: none (code `204`)
## `/api/pleroma/admin/reports` ## `GET /api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports
- Method `GET`
- Params: - Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved` - *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve - *optional* `limit`: **integer** the number of records to retrieve
@ -418,7 +398,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```json ```json
{ {
"total" : 1, "totalReports" : 1,
"reports": [ "reports": [
{ {
"account": { "account": {
@ -560,9 +540,34 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/reports/:id` ## `GET /api/pleroma/admin/grouped_reports`
### Get a list of reports, grouped by status
- Params: none
- On success: JSON, returns a list of reports, where:
- `date`: date of the latest report
- `account`: the user who has been reported (see `/api/pleroma/admin/reports` for reference)
- `status`: reported status (see `/api/pleroma/admin/reports` for reference)
- `actors`: users who had reported this status (see `/api/pleroma/admin/reports` for reference)
- `reports`: reports (see `/api/pleroma/admin/reports` for reference)
```json
"reports": [
{
"date": "2019-10-07T12:31:39.615149Z",
"account": { ... },
"status": { ... },
"actors": [{ ... }, { ... }],
"reports": [{ ... }]
}
]
```
## `GET /api/pleroma/admin/reports/:id`
### Get an individual report ### Get an individual report
- Method `GET`
- Params: - Params:
- `id` - `id`
- Response: - Response:
@ -571,22 +576,41 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: JSON, Report object (see above) - On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id` ## `PATCH /api/pleroma/admin/reports`
### Change the state of the report
- Method `PUT` ### Change the state of one or multiple reports
- Params: - Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved` ```json
`reports`: [
{
`id`, // required, report id
`state` // required, the new state. Valid values are `open`, `closed` and `resolved`
},
...
]
```
- Response: - Response:
- On failure: - On failure:
- 400 Bad Request `"Unsupported state"` - 400 Bad Request, JSON:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"` ```json
- On success: JSON, Report object (see above) [
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/respond`
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report ### Respond to a report
- Method `POST`
- Params: - Params:
- `id` - `id`
- `status`: required, the message - `status`: required, the message
@ -656,9 +680,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/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
- Method `PUT`
- Params: - Params:
- `id` - `id`
- `sensitive`: optional, valid values are `true` or `false` - `sensitive`: optional, valid values are `true` or `false`
@ -670,9 +695,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity - On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id` ## `DELETE /api/pleroma/admin/statuses/:id`
### Delete an individual reported status ### Delete an individual reported status
- Method `DELETE`
- Params: - Params:
- `id` - `id`
- Response: - Response:
@ -681,11 +707,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"` - 404 Not Found `"Not found"`
- On success: 200 OK `{}` - On success: 200 OK `{}`
## `GET /api/pleroma/admin/config/migrate_to_db`
## `/api/pleroma/admin/config/migrate_to_db`
### Run mix task pleroma.config migrate_to_db ### Run mix task pleroma.config migrate_to_db
Copy settings on key `:pleroma` to DB. Copy settings on key `:pleroma` to DB.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -693,10 +720,12 @@ Copy settings on key `:pleroma` to DB.
{} {}
``` ```
## `/api/pleroma/admin/config/migrate_from_db` ## `GET /api/pleroma/admin/config/migrate_from_db`
### Run mix task pleroma.config migrate_from_db ### Run mix task pleroma.config migrate_from_db
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -704,10 +733,12 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele
{} {}
``` ```
## `/api/pleroma/admin/config` ## `GET /api/pleroma/admin/config`
### List config settings ### List config settings
List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
- Method `GET`
- Params: none - Params: none
- Response: - Response:
@ -723,8 +754,10 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
} }
``` ```
## `/api/pleroma/admin/config` ## `POST /api/pleroma/admin/config`
### Update config settings ### Update config settings
Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`. Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`. Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
@ -747,7 +780,6 @@ Compile time settings (need instance reboot):
- `Pleroma.Upload` -> `:proxy_remote` - `Pleroma.Upload` -> `:proxy_remote`
- `:instance` -> `:upload_limit` - `:instance` -> `:upload_limit`
- Method `POST`
- Params: - Params:
- `configs` => [ - `configs` => [
- `group` (string) - `group` (string)
@ -802,9 +834,10 @@ Compile time settings (need instance reboot):
} }
``` ```
## `/api/pleroma/admin/moderation_log` ## `GET /api/pleroma/admin/moderation_log`
### Get moderation log ### Get moderation log
- Method `GET`
- Params: - Params:
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`) - *optional* `page_size`: **integer** number of log entries per page (default is `50`)
@ -831,8 +864,9 @@ Compile time settings (need instance reboot):
``` ```
## `POST /api/pleroma/admin/reload_emoji` ## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji ### Reload the instance's custom emoji
* Method `POST`
* Authentication: required - Authentication: required
* Params: None - Params: None
* Response: JSON, "ok" and 200 status - Response: JSON, "ok" and 200 status

View file

@ -479,3 +479,35 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `artist`: the artist of the media playing [optional] * `artist`: the artist of the media playing [optional]
* `length`: the length of the media playing [optional] * `length`: the length of the media playing [optional]
* Response: the newly created media metadata entity representing the Listen activity * Response: the newly created media metadata entity representing the Listen activity
# Emoji Reactions
Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character.
## `POST /api/v1/pleroma/statuses/:id/react_with_emoji`
### React to a post with a unicode emoji
* Method: `POST`
* Authentication: required
* Params: `emoji`: A single character unicode emoji
* Response: JSON, the status.
## `POST /api/v1/pleroma/statuses/:id/unreact_with_emoji`
### Remove a reaction to a post with a unicode emoji
* Method: `POST`
* Authentication: required
* Params: `emoji`: A single character unicode emoji
* Response: JSON, the status.
## `GET /api/v1/pleroma/statuses/:id/emoji_reactions_by`
### Get an object of emoji to account mappings with accounts that reacted to the post
* Method: `GET`
* Authentication: optional
* Params: None
* Response: JSON, a map of emoji to account list mappings.
* Example Response:
```json
{
"😀" => [{"id" => "xyz.."...}, {"id" => "zyx..."}],
"🗡" => [{"id" => "abc..."}]
}
```

View file

@ -2,6 +2,9 @@
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`. Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`.
!!! danger
These mix tasks can take a long time to complete. Many of them were written to address specific database issues that happened because of bugs in migrations or other specific scenarios. Do not run these tasks "just in case" if everything is fine your instance.
## Replace embedded objects with their references ## Replace embedded objects with their references
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration. Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.

View file

@ -41,6 +41,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed. * `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
@ -137,6 +138,13 @@ config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"] "example.org": ["https://example.org/users/admin"]
``` ```
#### :mrf_object_age
* `threshold`: Required age (in seconds) of a post before actions are taken.
* `actions`: A list of actions to apply to the post:
* `:delist` removes the post from public timelines
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely
### :activitypub ### :activitypub
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed * ``unfollow_blocked``: Whether blocks result in people getting unfollowed
* ``outgoing_blocks``: Whether to federate blocks to other instances * ``outgoing_blocks``: Whether to federate blocks to other instances
@ -648,7 +656,7 @@ Feel free to adjust the priv_dir and port number. Then you will have to create t
### :admin_token ### :admin_token
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example: Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter or `x-admin-token` HTTP header. Example:
```elixir ```elixir
config :pleroma, :admin_token, "somerandomtoken" config :pleroma, :admin_token, "somerandomtoken"
@ -656,8 +664,14 @@ config :pleroma, :admin_token, "somerandomtoken"
You can then do You can then do
```sh ```shell
curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerandomtoken" curl "http://localhost:4000/api/pleroma/admin/users/invites?admin_token=somerandomtoken"
```
or
```shell
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
``` ```
### :auth ### :auth

View file

@ -1,9 +1,13 @@
# Installing on OpenBSD # Installing on OpenBSD
This guide describes the installation and configuration of pleroma (and the required software to run it) on a single OpenBSD 6.4 server. This guide describes the installation and configuration of pleroma (and the required software to run it) on a single OpenBSD 6.4 server.
For any additional information regarding commands and configuration files mentioned here, check the man pages [online](https://man.openbsd.org/) or directly on your server with the man command. For any additional information regarding commands and configuration files mentioned here, check the man pages [online](https://man.openbsd.org/) or directly on your server with the man command.
#### Required software #### Required software
The following packages need to be installed: The following packages need to be installed:
* elixir * elixir
* gmake * gmake
* ImageMagick * ImageMagick
@ -12,7 +16,10 @@ The following packages need to be installed:
* postgresql-contrib * postgresql-contrib
To install them, run the following command (with doas or as root): To install them, run the following command (with doas or as root):
`pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib`
```
pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib
```
Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt.
@ -31,7 +38,7 @@ Create the \_pleroma user, assign it the pleroma login class and create its home
#### Clone pleroma's directory #### Clone pleroma's directory
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
#### Postgresql #### PostgreSQL
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
If you wish to not use the default location for postgresql's data (/var/postgresql/data), add the following switch at the end of the command: `-D <path>` and modify the `datadir` variable in the /etc/rc.d/postgresql script. If you wish to not use the default location for postgresql's data (/var/postgresql/data), add the following switch at the end of the command: `-D <path>` and modify the `datadir` variable in the /etc/rc.d/postgresql script.
@ -44,6 +51,7 @@ To check that it started properly and didn't fail right after starting, you can
#### httpd #### httpd
httpd will have three fuctions: httpd will have three fuctions:
* redirect requests trying to reach the instance over http to the https URL * redirect requests trying to reach the instance over http to the https URL
* serve a robots.txt file * serve a robots.txt file
* get Let's Encrypt certificates, with acme-client * get Let's Encrypt certificates, with acme-client
@ -76,7 +84,7 @@ types {
include "/usr/share/misc/mime.types" include "/usr/share/misc/mime.types"
} }
``` ```
Do not forget to change *\<IPv4/6 address\>* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options. Do not forget to change *<IPv4/6 address\>* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options.
Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt. Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt.
Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root): Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root):
@ -107,7 +115,7 @@ domain <domain name> {
challengedir "/var/www/acme/" challengedir "/var/www/acme/"
} }
``` ```
Replace *\<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` to create account and domain keys, and request a certificate for the first time. Replace *<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` to create account and domain keys, and request a certificate for the first time.
Make acme-client run everyday by adding it in /etc/daily.local. As root, run the following command: `echo "acme-client <domain name>" >> /etc/daily.local`. Make acme-client run everyday by adding it in /etc/daily.local. As root, run the following command: `echo "acme-client <domain name>" >> /etc/daily.local`.
Relayd will look for certificates and keys based on the address it listens on (see next part), the easiest way to make them available to relayd is to create a link, as root run: Relayd will look for certificates and keys based on the address it listens on (see next part), the easiest way to make them available to relayd is to create a link, as root run:
@ -169,7 +177,7 @@ relay wwwtls {
forward to <httpd_server> port 80 check http "/robots.txt" code 200 forward to <httpd_server> port 80 check http "/robots.txt" code 200
} }
``` ```
Again, change *\<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://\<your instance's domain name\>*. Again, change *<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://<your instance's domain name\>*.
Check the configuration with `relayd -n`, if it is OK enable and start relayd (as root): Check the configuration with `relayd -n`, if it is OK enable and start relayd (as root):
``` ```
rcctl enable relayd rcctl enable relayd
@ -202,13 +210,15 @@ pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach par
pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd
pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh
``` ```
Replace *\<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for exemple, your home IP address, to avoid SSH connection attempts from bots. Replace *<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for exemple, your home IP address, to avoid SSH connection attempts from bots.
Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`. Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`.
#### Configure and start pleroma #### Configure and start pleroma
Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`). Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`).
Then follow the main installation guide: Then follow the main installation guide:
* run `mix deps.get` * run `mix deps.get`
* run `mix pleroma.instance gen` and enter your instance's information when asked * run `mix pleroma.instance gen` and enter your instance's information when asked
* copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK. * copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK.

View file

@ -41,6 +41,10 @@ defmodule Pleroma.Activity do
field(:actor, :string) field(:actor, :string)
field(:recipients, {:array, :string}, default: []) field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true) field(:thread_muted?, :boolean, virtual: true)
# This is a fake relation,
# do not use outside of with_preloaded_user_actor/with_joined_user_actor
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark) has_one(:bookmark, Bookmark)
has_many(:notifications, Notification, on_delete: :delete_all) has_many(:notifications, Notification, on_delete: :delete_all)
@ -86,6 +90,19 @@ def with_preloaded_object(query, join_type \\ :inner) do
|> preload([activity, object: object], object: object) |> preload([activity, object: object], object: object)
end end
def with_joined_user_actor(query, join_type \\ :inner) do
join(query, join_type, [activity], u in User,
on: u.ap_id == activity.actor,
as: :user_actor
)
end
def with_preloaded_user_actor(query, join_type \\ :inner) do
query
|> with_joined_user_actor(join_type)
|> preload([activity, user_actor: user_actor], user_actor: user_actor)
end
def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, %User{} = user) do
from([a] in query, from([a] in query,
left_join: b in Bookmark, left_join: b in Bookmark,

View file

@ -9,6 +9,8 @@ defmodule Pleroma.Constants do
const(object_internal_fields, const(object_internal_fields,
do: [ do: [
"reactions",
"reaction_count",
"likes", "likes",
"like_count", "like_count",
"announcements", "announcements",

769
lib/pleroma/emoji-data.txt Normal file
View file

@ -0,0 +1,769 @@
# emoji-data.txt
# Date: 2019-01-15, 12:10:05 GMT
# © 2019 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html
#
# Emoji Data for UTS #51
# Version: 12.0
#
# For documentation and usage, see http://www.unicode.org/reports/tr51
#
# Format:
# <codepoint(s)> ; <property> # <comments>
# Note: there is no guarantee as to the structure of whitespace or comments
#
# Characters and sequences are listed in code point order. Users should be shown a more natural order.
# See the CLDR collation order for Emoji.
# ================================================
# All omitted code points have Emoji=No
# @missing: 0000..10FFFF ; Emoji ; No
0023 ; Emoji # 1.1 [1] (#) number sign
002A ; Emoji # 1.1 [1] (*) asterisk
0030..0039 ; Emoji # 1.1 [10] (0..9) digit zero..digit nine
00A9 ; Emoji # 1.1 [1] (©️) copyright
00AE ; Emoji # 1.1 [1] (®️) registered
203C ; Emoji # 1.1 [1] (‼️) double exclamation mark
2049 ; Emoji # 3.0 [1] (⁉️) exclamation question mark
2122 ; Emoji # 1.1 [1] (™️) trade mark
2139 ; Emoji # 3.0 [1] () information
2194..2199 ; Emoji # 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
21A9..21AA ; Emoji # 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
231A..231B ; Emoji # 1.1 [2] (⌚..⌛) watch..hourglass done
2328 ; Emoji # 1.1 [1] (⌨️) keyboard
23CF ; Emoji # 4.0 [1] (⏏️) eject button
23E9..23F3 ; Emoji # 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
23F8..23FA ; Emoji # 7.0 [3] (⏸️..⏺️) pause button..record button
24C2 ; Emoji # 1.1 [1] (Ⓜ️) circled M
25AA..25AB ; Emoji # 1.1 [2] (▪️..▫️) black small square..white small square
25B6 ; Emoji # 1.1 [1] (▶️) play button
25C0 ; Emoji # 1.1 [1] (◀️) reverse button
25FB..25FE ; Emoji # 3.2 [4] (◻️..◾) white medium square..black medium-small square
2600..2604 ; Emoji # 1.1 [5] (☀️..☄️) sun..comet
260E ; Emoji # 1.1 [1] (☎️) telephone
2611 ; Emoji # 1.1 [1] (☑️) check box with check
2614..2615 ; Emoji # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2618 ; Emoji # 4.1 [1] (☘️) shamrock
261D ; Emoji # 1.1 [1] (☝️) index pointing up
2620 ; Emoji # 1.1 [1] (☠️) skull and crossbones
2622..2623 ; Emoji # 1.1 [2] (☢️..☣️) radioactive..biohazard
2626 ; Emoji # 1.1 [1] (☦️) orthodox cross
262A ; Emoji # 1.1 [1] (☪️) star and crescent
262E..262F ; Emoji # 1.1 [2] (☮️..☯️) peace symbol..yin yang
2638..263A ; Emoji # 1.1 [3] (☸️..☺️) wheel of dharma..smiling face
2640 ; Emoji # 1.1 [1] (♀️) female sign
2642 ; Emoji # 1.1 [1] (♂️) male sign
2648..2653 ; Emoji # 1.1 [12] (♈..♓) Aries..Pisces
265F..2660 ; Emoji # 1.1 [2] (♟️..♠️) chess pawn..spade suit
2663 ; Emoji # 1.1 [1] (♣️) club suit
2665..2666 ; Emoji # 1.1 [2] (♥️..♦️) heart suit..diamond suit
2668 ; Emoji # 1.1 [1] (♨️) hot springs
267B ; Emoji # 3.2 [1] (♻️) recycling symbol
267E..267F ; Emoji # 4.1 [2] (♾️..♿) infinity..wheelchair symbol
2692..2697 ; Emoji # 4.1 [6] (⚒️..⚗️) hammer and pick..alembic
2699 ; Emoji # 4.1 [1] (⚙️) gear
269B..269C ; Emoji # 4.1 [2] (⚛️..⚜️) atom symbol..fleur-de-lis
26A0..26A1 ; Emoji # 4.0 [2] (⚠️..⚡) warning..high voltage
26AA..26AB ; Emoji # 4.1 [2] (⚪..⚫) white circle..black circle
26B0..26B1 ; Emoji # 4.1 [2] (⚰️..⚱️) coffin..funeral urn
26BD..26BE ; Emoji # 5.2 [2] (⚽..⚾) soccer ball..baseball
26C4..26C5 ; Emoji # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
26C8 ; Emoji # 5.2 [1] (⛈️) cloud with lightning and rain
26CE ; Emoji # 6.0 [1] (⛎) Ophiuchus
26CF ; Emoji # 5.2 [1] (⛏️) pick
26D1 ; Emoji # 5.2 [1] (⛑️) rescue workers helmet
26D3..26D4 ; Emoji # 5.2 [2] (⛓️..⛔) chains..no entry
26E9..26EA ; Emoji # 5.2 [2] (⛩️..⛪) shinto shrine..church
26F0..26F5 ; Emoji # 5.2 [6] (⛰️..⛵) mountain..sailboat
26F7..26FA ; Emoji # 5.2 [4] (⛷️..⛺) skier..tent
26FD ; Emoji # 5.2 [1] (⛽) fuel pump
2702 ; Emoji # 1.1 [1] (✂️) scissors
2705 ; Emoji # 6.0 [1] (✅) check mark button
2708..2709 ; Emoji # 1.1 [2] (✈️..✉️) airplane..envelope
270A..270B ; Emoji # 6.0 [2] (✊..✋) raised fist..raised hand
270C..270D ; Emoji # 1.1 [2] (✌️..✍️) victory hand..writing hand
270F ; Emoji # 1.1 [1] (✏️) pencil
2712 ; Emoji # 1.1 [1] (✒️) black nib
2714 ; Emoji # 1.1 [1] (✔️) check mark
2716 ; Emoji # 1.1 [1] (✖️) multiplication sign
271D ; Emoji # 1.1 [1] (✝️) latin cross
2721 ; Emoji # 1.1 [1] (✡️) star of David
2728 ; Emoji # 6.0 [1] (✨) sparkles
2733..2734 ; Emoji # 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
2744 ; Emoji # 1.1 [1] (❄️) snowflake
2747 ; Emoji # 1.1 [1] (❇️) sparkle
274C ; Emoji # 6.0 [1] (❌) cross mark
274E ; Emoji # 6.0 [1] (❎) cross mark button
2753..2755 ; Emoji # 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Emoji # 5.2 [1] (❗) exclamation mark
2763..2764 ; Emoji # 1.1 [2] (❣️..❤️) heart exclamation..red heart
2795..2797 ; Emoji # 6.0 [3] (..➗) plus sign..division sign
27A1 ; Emoji # 1.1 [1] (➡️) right arrow
27B0 ; Emoji # 6.0 [1] (➰) curly loop
27BF ; Emoji # 6.0 [1] (➿) double curly loop
2934..2935 ; Emoji # 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
2B05..2B07 ; Emoji # 4.0 [3] (⬅️..⬇️) left arrow..down arrow
2B1B..2B1C ; Emoji # 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Emoji # 5.1 [1] (⭐) star
2B55 ; Emoji # 5.2 [1] (⭕) hollow red circle
3030 ; Emoji # 1.1 [1] (〰️) wavy dash
303D ; Emoji # 3.2 [1] (〽️) part alternation mark
3297 ; Emoji # 1.1 [1] (㊗️) Japanese “congratulations” button
3299 ; Emoji # 1.1 [1] (㊙️) Japanese “secret” button
1F004 ; Emoji # 5.1 [1] (🀄) mahjong red dragon
1F0CF ; Emoji # 6.0 [1] (🃏) joker
1F170..1F171 ; Emoji # 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
1F17E ; Emoji # 6.0 [1] (🅾️) O button (blood type)
1F17F ; Emoji # 5.2 [1] (🅿️) P button
1F18E ; Emoji # 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Emoji # 6.0 [10] (🆑..🆚) CL button..VS button
1F1E6..1F1FF ; Emoji # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F201..1F202 ; Emoji # 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
1F21A ; Emoji # 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Emoji # 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F23A ; Emoji # 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
1F250..1F251 ; Emoji # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F300..1F320 ; Emoji # 6.0 [33] (🌀..🌠) cyclone..shooting star
1F321 ; Emoji # 7.0 [1] (🌡️) thermometer
1F324..1F32C ; Emoji # 7.0 [9] (🌤️..🌬️) sun behind small cloud..wind face
1F32D..1F32F ; Emoji # 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Emoji # 6.0 [6] (🌰..🌵) chestnut..cactus
1F336 ; Emoji # 7.0 [1] (🌶️) hot pepper
1F337..1F37C ; Emoji # 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37D ; Emoji # 7.0 [1] (🍽️) fork and knife with plate
1F37E..1F37F ; Emoji # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Emoji # 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F396..1F397 ; Emoji # 7.0 [2] (🎖️..🎗️) military medal..reminder ribbon
1F399..1F39B ; Emoji # 7.0 [3] (🎙️..🎛️) studio microphone..control knobs
1F39E..1F39F ; Emoji # 7.0 [2] (🎞️..🎟️) film frames..admission tickets
1F3A0..1F3C4 ; Emoji # 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Emoji # 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Emoji # 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CB..1F3CE ; Emoji # 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
1F3CF..1F3D3 ; Emoji # 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3D4..1F3DF ; Emoji # 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
1F3E0..1F3F0 ; Emoji # 6.0 [17] (🏠..🏰) house..castle
1F3F3..1F3F5 ; Emoji # 7.0 [3] (🏳️..🏵️) white flag..rosette
1F3F7 ; Emoji # 7.0 [1] (🏷️) label
1F3F8..1F3FF ; Emoji # 8.0 [8] (🏸..🏿) badminton..dark skin tone
1F400..1F43E ; Emoji # 6.0 [63] (🐀..🐾) rat..paw prints
1F43F ; Emoji # 7.0 [1] (🐿️) chipmunk
1F440 ; Emoji # 6.0 [1] (👀) eyes
1F441 ; Emoji # 7.0 [1] (👁️) eye
1F442..1F4F7 ; Emoji # 6.0[182] (👂..📷) ear..camera
1F4F8 ; Emoji # 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Emoji # 6.0 [4] (📹..📼) video camera..videocassette
1F4FD ; Emoji # 7.0 [1] (📽️) film projector
1F4FF ; Emoji # 8.0 [1] (📿) prayer beads
1F500..1F53D ; Emoji # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F549..1F54A ; Emoji # 7.0 [2] (🕉️..🕊️) om..dove
1F54B..1F54E ; Emoji # 8.0 [4] (🕋..🕎) kaaba..menorah
1F550..1F567 ; Emoji # 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F56F..1F570 ; Emoji # 7.0 [2] (🕯️..🕰️) candle..mantelpiece clock
1F573..1F579 ; Emoji # 7.0 [7] (🕳️..🕹️) hole..joystick
1F57A ; Emoji # 9.0 [1] (🕺) man dancing
1F587 ; Emoji # 7.0 [1] (🖇️) linked paperclips
1F58A..1F58D ; Emoji # 7.0 [4] (🖊️..🖍️) pen..crayon
1F590 ; Emoji # 7.0 [1] (🖐️) hand with fingers splayed
1F595..1F596 ; Emoji # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F5A4 ; Emoji # 9.0 [1] (🖤) black heart
1F5A5 ; Emoji # 7.0 [1] (🖥️) desktop computer
1F5A8 ; Emoji # 7.0 [1] (🖨️) printer
1F5B1..1F5B2 ; Emoji # 7.0 [2] (🖱️..🖲️) computer mouse..trackball
1F5BC ; Emoji # 7.0 [1] (🖼️) framed picture
1F5C2..1F5C4 ; Emoji # 7.0 [3] (🗂️..🗄️) card index dividers..file cabinet
1F5D1..1F5D3 ; Emoji # 7.0 [3] (🗑️..🗓️) wastebasket..spiral calendar
1F5DC..1F5DE ; Emoji # 7.0 [3] (🗜️..🗞️) clamp..rolled-up newspaper
1F5E1 ; Emoji # 7.0 [1] (🗡️) dagger
1F5E3 ; Emoji # 7.0 [1] (🗣️) speaking head
1F5E8 ; Emoji # 7.0 [1] (🗨️) left speech bubble
1F5EF ; Emoji # 7.0 [1] (🗯️) right anger bubble
1F5F3 ; Emoji # 7.0 [1] (🗳️) ballot box with ballot
1F5FA ; Emoji # 7.0 [1] (🗺️) world map
1F5FB..1F5FF ; Emoji # 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Emoji # 6.1 [1] (😀) grinning face
1F601..1F610 ; Emoji # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Emoji # 6.1 [1] (😑) expressionless face
1F612..1F614 ; Emoji # 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Emoji # 6.1 [1] (😕) confused face
1F616 ; Emoji # 6.0 [1] (😖) confounded face
1F617 ; Emoji # 6.1 [1] (😗) kissing face
1F618 ; Emoji # 6.0 [1] (😘) face blowing a kiss
1F619 ; Emoji # 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Emoji # 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Emoji # 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Emoji # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Emoji # 6.1 [1] (😟) worried face
1F620..1F625 ; Emoji # 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Emoji # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Emoji # 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Emoji # 6.1 [1] (😬) grimacing face
1F62D ; Emoji # 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Emoji # 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Emoji # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Emoji # 6.1 [1] (😴) sleeping face
1F635..1F640 ; Emoji # 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Emoji # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Emoji # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Emoji # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Emoji # 6.0 [70] (🚀..🛅) rocket..left luggage
1F6CB..1F6CF ; Emoji # 7.0 [5] (🛋️..🛏️) couch and lamp..bed
1F6D0 ; Emoji # 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Emoji # 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D5 ; Emoji # 12.0 [1] (🛕) hindu temple
1F6E0..1F6E5 ; Emoji # 7.0 [6] (🛠️..🛥️) hammer and wrench..motor boat
1F6E9 ; Emoji # 7.0 [1] (🛩️) small airplane
1F6EB..1F6EC ; Emoji # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
1F6F0 ; Emoji # 7.0 [1] (🛰️) satellite
1F6F3 ; Emoji # 7.0 [1] (🛳️) passenger ship
1F6F4..1F6F6 ; Emoji # 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Emoji # 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Emoji # 11.0 [1] (🛹) skateboard
1F6FA ; Emoji # 12.0 [1] (🛺) auto rickshaw
1F7E0..1F7EB ; Emoji # 12.0 [12] (🟠..🟫) orange circle..brown square
1F90D..1F90F ; Emoji # 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Emoji # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Emoji # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji # 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Emoji # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Emoji # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Emoji # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Emoji # 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Emoji # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Emoji # 12.0 [1] (🤿) diving mask
1F940..1F945 ; Emoji # 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Emoji # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Emoji # 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Emoji # 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Emoji # 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Emoji # 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Emoji # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Emoji # 12.0 [1] (🥱) yawning face
1F973..1F976 ; Emoji # 11.0 [4] (🥳..🥶) partying face..cold face
1F97A ; Emoji # 11.0 [1] (🥺) pleading face
1F97B ; Emoji # 12.0 [1] (🥻) sari
1F97C..1F97F ; Emoji # 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Emoji # 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Emoji # 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Emoji # 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Emoji # 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A5..1F9AA ; Emoji # 12.0 [6] (🦥..🦪) sloth..oyster
1F9AE..1F9AF ; Emoji # 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Emoji # 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Emoji # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Emoji # 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Emoji # 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Emoji # 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CD..1F9CF ; Emoji # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Emoji # 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Emoji # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA70..1FA73 ; Emoji # 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA78..1FA7A ; Emoji # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA80..1FA82 ; Emoji # 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA90..1FA95 ; Emoji # 12.0 [6] (🪐..🪕) ringed planet..banjo
# Total elements: 1311
# ================================================
# All omitted code points have Emoji_Presentation=No
# @missing: 0000..10FFFF ; Emoji_Presentation ; No
231A..231B ; Emoji_Presentation # 1.1 [2] (⌚..⌛) watch..hourglass done
23E9..23EC ; Emoji_Presentation # 6.0 [4] (⏩..⏬) fast-forward button..fast down button
23F0 ; Emoji_Presentation # 6.0 [1] (⏰) alarm clock
23F3 ; Emoji_Presentation # 6.0 [1] (⏳) hourglass not done
25FD..25FE ; Emoji_Presentation # 3.2 [2] (◽..◾) white medium-small square..black medium-small square
2614..2615 ; Emoji_Presentation # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2648..2653 ; Emoji_Presentation # 1.1 [12] (♈..♓) Aries..Pisces
267F ; Emoji_Presentation # 4.1 [1] (♿) wheelchair symbol
2693 ; Emoji_Presentation # 4.1 [1] (⚓) anchor
26A1 ; Emoji_Presentation # 4.0 [1] (⚡) high voltage
26AA..26AB ; Emoji_Presentation # 4.1 [2] (⚪..⚫) white circle..black circle
26BD..26BE ; Emoji_Presentation # 5.2 [2] (⚽..⚾) soccer ball..baseball
26C4..26C5 ; Emoji_Presentation # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
26CE ; Emoji_Presentation # 6.0 [1] (⛎) Ophiuchus
26D4 ; Emoji_Presentation # 5.2 [1] (⛔) no entry
26EA ; Emoji_Presentation # 5.2 [1] (⛪) church
26F2..26F3 ; Emoji_Presentation # 5.2 [2] (⛲..⛳) fountain..flag in hole
26F5 ; Emoji_Presentation # 5.2 [1] (⛵) sailboat
26FA ; Emoji_Presentation # 5.2 [1] (⛺) tent
26FD ; Emoji_Presentation # 5.2 [1] (⛽) fuel pump
2705 ; Emoji_Presentation # 6.0 [1] (✅) check mark button
270A..270B ; Emoji_Presentation # 6.0 [2] (✊..✋) raised fist..raised hand
2728 ; Emoji_Presentation # 6.0 [1] (✨) sparkles
274C ; Emoji_Presentation # 6.0 [1] (❌) cross mark
274E ; Emoji_Presentation # 6.0 [1] (❎) cross mark button
2753..2755 ; Emoji_Presentation # 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Emoji_Presentation # 5.2 [1] (❗) exclamation mark
2795..2797 ; Emoji_Presentation # 6.0 [3] (..➗) plus sign..division sign
27B0 ; Emoji_Presentation # 6.0 [1] (➰) curly loop
27BF ; Emoji_Presentation # 6.0 [1] (➿) double curly loop
2B1B..2B1C ; Emoji_Presentation # 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Emoji_Presentation # 5.1 [1] (⭐) star
2B55 ; Emoji_Presentation # 5.2 [1] (⭕) hollow red circle
1F004 ; Emoji_Presentation # 5.1 [1] (🀄) mahjong red dragon
1F0CF ; Emoji_Presentation # 6.0 [1] (🃏) joker
1F18E ; Emoji_Presentation # 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Emoji_Presentation # 6.0 [10] (🆑..🆚) CL button..VS button
1F1E6..1F1FF ; Emoji_Presentation # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F201 ; Emoji_Presentation # 6.0 [1] (🈁) Japanese “here” button
1F21A ; Emoji_Presentation # 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Emoji_Presentation # 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F236 ; Emoji_Presentation # 6.0 [5] (🈲..🈶) Japanese “prohibited” button..Japanese “not free of charge” button
1F238..1F23A ; Emoji_Presentation # 6.0 [3] (🈸..🈺) Japanese “application” button..Japanese “open for business” button
1F250..1F251 ; Emoji_Presentation # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F300..1F320 ; Emoji_Presentation # 6.0 [33] (🌀..🌠) cyclone..shooting star
1F32D..1F32F ; Emoji_Presentation # 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Emoji_Presentation # 6.0 [6] (🌰..🌵) chestnut..cactus
1F337..1F37C ; Emoji_Presentation # 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37E..1F37F ; Emoji_Presentation # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Emoji_Presentation # 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F3A0..1F3C4 ; Emoji_Presentation # 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Emoji_Presentation # 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Emoji_Presentation # 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CF..1F3D3 ; Emoji_Presentation # 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3E0..1F3F0 ; Emoji_Presentation # 6.0 [17] (🏠..🏰) house..castle
1F3F4 ; Emoji_Presentation # 7.0 [1] (🏴) black flag
1F3F8..1F3FF ; Emoji_Presentation # 8.0 [8] (🏸..🏿) badminton..dark skin tone
1F400..1F43E ; Emoji_Presentation # 6.0 [63] (🐀..🐾) rat..paw prints
1F440 ; Emoji_Presentation # 6.0 [1] (👀) eyes
1F442..1F4F7 ; Emoji_Presentation # 6.0[182] (👂..📷) ear..camera
1F4F8 ; Emoji_Presentation # 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Emoji_Presentation # 6.0 [4] (📹..📼) video camera..videocassette
1F4FF ; Emoji_Presentation # 8.0 [1] (📿) prayer beads
1F500..1F53D ; Emoji_Presentation # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F54B..1F54E ; Emoji_Presentation # 8.0 [4] (🕋..🕎) kaaba..menorah
1F550..1F567 ; Emoji_Presentation # 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F57A ; Emoji_Presentation # 9.0 [1] (🕺) man dancing
1F595..1F596 ; Emoji_Presentation # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F5A4 ; Emoji_Presentation # 9.0 [1] (🖤) black heart
1F5FB..1F5FF ; Emoji_Presentation # 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Emoji_Presentation # 6.1 [1] (😀) grinning face
1F601..1F610 ; Emoji_Presentation # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Emoji_Presentation # 6.1 [1] (😑) expressionless face
1F612..1F614 ; Emoji_Presentation # 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Emoji_Presentation # 6.1 [1] (😕) confused face
1F616 ; Emoji_Presentation # 6.0 [1] (😖) confounded face
1F617 ; Emoji_Presentation # 6.1 [1] (😗) kissing face
1F618 ; Emoji_Presentation # 6.0 [1] (😘) face blowing a kiss
1F619 ; Emoji_Presentation # 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Emoji_Presentation # 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Emoji_Presentation # 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Emoji_Presentation # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Emoji_Presentation # 6.1 [1] (😟) worried face
1F620..1F625 ; Emoji_Presentation # 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Emoji_Presentation # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Emoji_Presentation # 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Emoji_Presentation # 6.1 [1] (😬) grimacing face
1F62D ; Emoji_Presentation # 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Emoji_Presentation # 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Emoji_Presentation # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Emoji_Presentation # 6.1 [1] (😴) sleeping face
1F635..1F640 ; Emoji_Presentation # 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Emoji_Presentation # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Emoji_Presentation # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Emoji_Presentation # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Emoji_Presentation # 6.0 [70] (🚀..🛅) rocket..left luggage
1F6CC ; Emoji_Presentation # 7.0 [1] (🛌) person in bed
1F6D0 ; Emoji_Presentation # 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Emoji_Presentation # 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D5 ; Emoji_Presentation # 12.0 [1] (🛕) hindu temple
1F6EB..1F6EC ; Emoji_Presentation # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
1F6F4..1F6F6 ; Emoji_Presentation # 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Emoji_Presentation # 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Emoji_Presentation # 11.0 [1] (🛹) skateboard
1F6FA ; Emoji_Presentation # 12.0 [1] (🛺) auto rickshaw
1F7E0..1F7EB ; Emoji_Presentation # 12.0 [12] (🟠..🟫) orange circle..brown square
1F90D..1F90F ; Emoji_Presentation # 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Emoji_Presentation # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Emoji_Presentation # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji_Presentation # 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Emoji_Presentation # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Emoji_Presentation # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Emoji_Presentation # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji_Presentation # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Emoji_Presentation # 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Emoji_Presentation # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Emoji_Presentation # 12.0 [1] (🤿) diving mask
1F940..1F945 ; Emoji_Presentation # 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Emoji_Presentation # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Emoji_Presentation # 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Emoji_Presentation # 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Emoji_Presentation # 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Emoji_Presentation # 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Emoji_Presentation # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Emoji_Presentation # 12.0 [1] (🥱) yawning face
1F973..1F976 ; Emoji_Presentation # 11.0 [4] (🥳..🥶) partying face..cold face
1F97A ; Emoji_Presentation # 11.0 [1] (🥺) pleading face
1F97B ; Emoji_Presentation # 12.0 [1] (🥻) sari
1F97C..1F97F ; Emoji_Presentation # 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Emoji_Presentation # 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Emoji_Presentation # 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Emoji_Presentation # 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Emoji_Presentation # 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A5..1F9AA ; Emoji_Presentation # 12.0 [6] (🦥..🦪) sloth..oyster
1F9AE..1F9AF ; Emoji_Presentation # 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Emoji_Presentation # 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Emoji_Presentation # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Emoji_Presentation # 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Emoji_Presentation # 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Emoji_Presentation # 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CD..1F9CF ; Emoji_Presentation # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Emoji_Presentation # 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Emoji_Presentation # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA70..1FA73 ; Emoji_Presentation # 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA78..1FA7A ; Emoji_Presentation # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA80..1FA82 ; Emoji_Presentation # 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA90..1FA95 ; Emoji_Presentation # 12.0 [6] (🪐..🪕) ringed planet..banjo
# Total elements: 1093
# ================================================
# All omitted code points have Emoji_Modifier=No
# @missing: 0000..10FFFF ; Emoji_Modifier ; No
1F3FB..1F3FF ; Emoji_Modifier # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
# Total elements: 5
# ================================================
# All omitted code points have Emoji_Modifier_Base=No
# @missing: 0000..10FFFF ; Emoji_Modifier_Base ; No
261D ; Emoji_Modifier_Base # 1.1 [1] (☝️) index pointing up
26F9 ; Emoji_Modifier_Base # 5.2 [1] (⛹️) person bouncing ball
270A..270B ; Emoji_Modifier_Base # 6.0 [2] (✊..✋) raised fist..raised hand
270C..270D ; Emoji_Modifier_Base # 1.1 [2] (✌️..✍️) victory hand..writing hand
1F385 ; Emoji_Modifier_Base # 6.0 [1] (🎅) Santa Claus
1F3C2..1F3C4 ; Emoji_Modifier_Base # 6.0 [3] (🏂..🏄) snowboarder..person surfing
1F3C7 ; Emoji_Modifier_Base # 6.0 [1] (🏇) horse racing
1F3CA ; Emoji_Modifier_Base # 6.0 [1] (🏊) person swimming
1F3CB..1F3CC ; Emoji_Modifier_Base # 7.0 [2] (🏋️..🏌️) person lifting weights..person golfing
1F442..1F443 ; Emoji_Modifier_Base # 6.0 [2] (👂..👃) ear..nose
1F446..1F450 ; Emoji_Modifier_Base # 6.0 [11] (👆..👐) backhand index pointing up..open hands
1F466..1F478 ; Emoji_Modifier_Base # 6.0 [19] (👦..👸) boy..princess
1F47C ; Emoji_Modifier_Base # 6.0 [1] (👼) baby angel
1F481..1F483 ; Emoji_Modifier_Base # 6.0 [3] (💁..💃) person tipping hand..woman dancing
1F485..1F487 ; Emoji_Modifier_Base # 6.0 [3] (💅..💇) nail polish..person getting haircut
1F48F ; Emoji_Modifier_Base # 6.0 [1] (💏) kiss
1F491 ; Emoji_Modifier_Base # 6.0 [1] (💑) couple with heart
1F4AA ; Emoji_Modifier_Base # 6.0 [1] (💪) flexed biceps
1F574..1F575 ; Emoji_Modifier_Base # 7.0 [2] (🕴️..🕵️) man in suit levitating..detective
1F57A ; Emoji_Modifier_Base # 9.0 [1] (🕺) man dancing
1F590 ; Emoji_Modifier_Base # 7.0 [1] (🖐️) hand with fingers splayed
1F595..1F596 ; Emoji_Modifier_Base # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
1F645..1F647 ; Emoji_Modifier_Base # 6.0 [3] (🙅..🙇) person gesturing NO..person bowing
1F64B..1F64F ; Emoji_Modifier_Base # 6.0 [5] (🙋..🙏) person raising hand..folded hands
1F6A3 ; Emoji_Modifier_Base # 6.0 [1] (🚣) person rowing boat
1F6B4..1F6B6 ; Emoji_Modifier_Base # 6.0 [3] (🚴..🚶) person biking..person walking
1F6C0 ; Emoji_Modifier_Base # 6.0 [1] (🛀) person taking bath
1F6CC ; Emoji_Modifier_Base # 7.0 [1] (🛌) person in bed
1F90F ; Emoji_Modifier_Base # 12.0 [1] (🤏) pinching hand
1F918 ; Emoji_Modifier_Base # 8.0 [1] (🤘) sign of the horns
1F919..1F91E ; Emoji_Modifier_Base # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Emoji_Modifier_Base # 10.0 [1] (🤟) love-you gesture
1F926 ; Emoji_Modifier_Base # 9.0 [1] (🤦) person facepalming
1F930 ; Emoji_Modifier_Base # 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Emoji_Modifier_Base # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F939 ; Emoji_Modifier_Base # 9.0 [7] (🤳..🤹) selfie..person juggling
1F93C..1F93E ; Emoji_Modifier_Base # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F9B5..1F9B6 ; Emoji_Modifier_Base # 11.0 [2] (🦵..🦶) leg..foot
1F9B8..1F9B9 ; Emoji_Modifier_Base # 11.0 [2] (🦸..🦹) superhero..supervillain
1F9BB ; Emoji_Modifier_Base # 12.0 [1] (🦻) ear with hearing aid
1F9CD..1F9CF ; Emoji_Modifier_Base # 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D1..1F9DD ; Emoji_Modifier_Base # 10.0 [13] (🧑..🧝) person..elf
# Total elements: 120
# ================================================
# All omitted code points have Emoji_Component=No
# @missing: 0000..10FFFF ; Emoji_Component ; No
0023 ; Emoji_Component # 1.1 [1] (#) number sign
002A ; Emoji_Component # 1.1 [1] (*) asterisk
0030..0039 ; Emoji_Component # 1.1 [10] (0..9) digit zero..digit nine
200D ; Emoji_Component # 1.1 [1] () zero width joiner
20E3 ; Emoji_Component # 3.0 [1] (⃣) combining enclosing keycap
FE0F ; Emoji_Component # 3.2 [1] () VARIATION SELECTOR-16
1F1E6..1F1FF ; Emoji_Component # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
1F3FB..1F3FF ; Emoji_Component # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
1F9B0..1F9B3 ; Emoji_Component # 11.0 [4] (🦰..🦳) red hair..white hair
E0020..E007F ; Emoji_Component # 3.1 [96] (󠀠..󠁿) tag space..cancel tag
# Total elements: 146
# ================================================
# All omitted code points have Extended_Pictographic=No
# @missing: 0000..10FFFF ; Extended_Pictographic ; No
00A9 ; Extended_Pictographic# 1.1 [1] (©️) copyright
00AE ; Extended_Pictographic# 1.1 [1] (®️) registered
203C ; Extended_Pictographic# 1.1 [1] (‼️) double exclamation mark
2049 ; Extended_Pictographic# 3.0 [1] (⁉️) exclamation question mark
2122 ; Extended_Pictographic# 1.1 [1] (™️) trade mark
2139 ; Extended_Pictographic# 3.0 [1] () information
2194..2199 ; Extended_Pictographic# 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
21A9..21AA ; Extended_Pictographic# 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
231A..231B ; Extended_Pictographic# 1.1 [2] (⌚..⌛) watch..hourglass done
2328 ; Extended_Pictographic# 1.1 [1] (⌨️) keyboard
2388 ; Extended_Pictographic# 3.0 [1] (⎈) HELM SYMBOL
23CF ; Extended_Pictographic# 4.0 [1] (⏏️) eject button
23E9..23F3 ; Extended_Pictographic# 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
23F8..23FA ; Extended_Pictographic# 7.0 [3] (⏸️..⏺️) pause button..record button
24C2 ; Extended_Pictographic# 1.1 [1] (Ⓜ️) circled M
25AA..25AB ; Extended_Pictographic# 1.1 [2] (▪️..▫️) black small square..white small square
25B6 ; Extended_Pictographic# 1.1 [1] (▶️) play button
25C0 ; Extended_Pictographic# 1.1 [1] (◀️) reverse button
25FB..25FE ; Extended_Pictographic# 3.2 [4] (◻️..◾) white medium square..black medium-small square
2600..2605 ; Extended_Pictographic# 1.1 [6] (☀️..★) sun..BLACK STAR
2607..2612 ; Extended_Pictographic# 1.1 [12] (☇..☒) LIGHTNING..BALLOT BOX WITH X
2614..2615 ; Extended_Pictographic# 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
2616..2617 ; Extended_Pictographic# 3.2 [2] (☖..☗) WHITE SHOGI PIECE..BLACK SHOGI PIECE
2618 ; Extended_Pictographic# 4.1 [1] (☘️) shamrock
2619 ; Extended_Pictographic# 3.0 [1] (☙) REVERSED ROTATED FLORAL HEART BULLET
261A..266F ; Extended_Pictographic# 1.1 [86] (☚..♯) BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN
2670..2671 ; Extended_Pictographic# 3.0 [2] (♰..♱) WEST SYRIAC CROSS..EAST SYRIAC CROSS
2672..267D ; Extended_Pictographic# 3.2 [12] (♲..♽) UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL
267E..267F ; Extended_Pictographic# 4.1 [2] (♾️..♿) infinity..wheelchair symbol
2680..2685 ; Extended_Pictographic# 3.2 [6] (⚀..⚅) DIE FACE-1..DIE FACE-6
2690..2691 ; Extended_Pictographic# 4.0 [2] (⚐..⚑) WHITE FLAG..BLACK FLAG
2692..269C ; Extended_Pictographic# 4.1 [11] (⚒️..⚜️) hammer and pick..fleur-de-lis
269D ; Extended_Pictographic# 5.1 [1] (⚝) OUTLINED WHITE STAR
269E..269F ; Extended_Pictographic# 5.2 [2] (⚞..⚟) THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT
26A0..26A1 ; Extended_Pictographic# 4.0 [2] (⚠️..⚡) warning..high voltage
26A2..26B1 ; Extended_Pictographic# 4.1 [16] (⚢..⚱️) DOUBLED FEMALE SIGN..funeral urn
26B2 ; Extended_Pictographic# 5.0 [1] (⚲) NEUTER
26B3..26BC ; Extended_Pictographic# 5.1 [10] (⚳..⚼) CERES..SESQUIQUADRATE
26BD..26BF ; Extended_Pictographic# 5.2 [3] (⚽..⚿) soccer ball..SQUARED KEY
26C0..26C3 ; Extended_Pictographic# 5.1 [4] (⛀..⛃) WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING
26C4..26CD ; Extended_Pictographic# 5.2 [10] (⛄..⛍) snowman without snow..DISABLED CAR
26CE ; Extended_Pictographic# 6.0 [1] (⛎) Ophiuchus
26CF..26E1 ; Extended_Pictographic# 5.2 [19] (⛏️..⛡) pick..RESTRICTED LEFT ENTRY-2
26E2 ; Extended_Pictographic# 6.0 [1] (⛢) ASTRONOMICAL SYMBOL FOR URANUS
26E3 ; Extended_Pictographic# 5.2 [1] (⛣) HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE
26E4..26E7 ; Extended_Pictographic# 6.0 [4] (⛤..⛧) PENTAGRAM..INVERTED PENTAGRAM
26E8..26FF ; Extended_Pictographic# 5.2 [24] (⛨..⛿) BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE
2700 ; Extended_Pictographic# 7.0 [1] (✀) BLACK SAFETY SCISSORS
2701..2704 ; Extended_Pictographic# 1.1 [4] (✁..✄) UPPER BLADE SCISSORS..WHITE SCISSORS
2705 ; Extended_Pictographic# 6.0 [1] (✅) check mark button
2708..2709 ; Extended_Pictographic# 1.1 [2] (✈️..✉️) airplane..envelope
270A..270B ; Extended_Pictographic# 6.0 [2] (✊..✋) raised fist..raised hand
270C..2712 ; Extended_Pictographic# 1.1 [7] (✌️..✒️) victory hand..black nib
2714 ; Extended_Pictographic# 1.1 [1] (✔️) check mark
2716 ; Extended_Pictographic# 1.1 [1] (✖️) multiplication sign
271D ; Extended_Pictographic# 1.1 [1] (✝️) latin cross
2721 ; Extended_Pictographic# 1.1 [1] (✡️) star of David
2728 ; Extended_Pictographic# 6.0 [1] (✨) sparkles
2733..2734 ; Extended_Pictographic# 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
2744 ; Extended_Pictographic# 1.1 [1] (❄️) snowflake
2747 ; Extended_Pictographic# 1.1 [1] (❇️) sparkle
274C ; Extended_Pictographic# 6.0 [1] (❌) cross mark
274E ; Extended_Pictographic# 6.0 [1] (❎) cross mark button
2753..2755 ; Extended_Pictographic# 6.0 [3] (❓..❕) question mark..white exclamation mark
2757 ; Extended_Pictographic# 5.2 [1] (❗) exclamation mark
2763..2767 ; Extended_Pictographic# 1.1 [5] (❣️..❧) heart exclamation..ROTATED FLORAL HEART BULLET
2795..2797 ; Extended_Pictographic# 6.0 [3] (..➗) plus sign..division sign
27A1 ; Extended_Pictographic# 1.1 [1] (➡️) right arrow
27B0 ; Extended_Pictographic# 6.0 [1] (➰) curly loop
27BF ; Extended_Pictographic# 6.0 [1] (➿) double curly loop
2934..2935 ; Extended_Pictographic# 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
2B05..2B07 ; Extended_Pictographic# 4.0 [3] (⬅️..⬇️) left arrow..down arrow
2B1B..2B1C ; Extended_Pictographic# 5.1 [2] (⬛..⬜) black large square..white large square
2B50 ; Extended_Pictographic# 5.1 [1] (⭐) star
2B55 ; Extended_Pictographic# 5.2 [1] (⭕) hollow red circle
3030 ; Extended_Pictographic# 1.1 [1] (〰️) wavy dash
303D ; Extended_Pictographic# 3.2 [1] (〽️) part alternation mark
3297 ; Extended_Pictographic# 1.1 [1] (㊗️) Japanese “congratulations” button
3299 ; Extended_Pictographic# 1.1 [1] (㊙️) Japanese “secret” button
1F000..1F02B ; Extended_Pictographic# 5.1 [44] (🀀..🀫) MAHJONG TILE EAST WIND..MAHJONG TILE BACK
1F02C..1F02F ; Extended_Pictographic# NA [4] (🀬..🀯) <reserved-1F02C>..<reserved-1F02F>
1F030..1F093 ; Extended_Pictographic# 5.1[100] (🀰..🂓) DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06
1F094..1F09F ; Extended_Pictographic# NA [12] (🂔..🂟) <reserved-1F094>..<reserved-1F09F>
1F0A0..1F0AE ; Extended_Pictographic# 6.0 [15] (🂠..🂮) PLAYING CARD BACK..PLAYING CARD KING OF SPADES
1F0AF..1F0B0 ; Extended_Pictographic# NA [2] (🂯..🂰) <reserved-1F0AF>..<reserved-1F0B0>
1F0B1..1F0BE ; Extended_Pictographic# 6.0 [14] (🂱..🂾) PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS
1F0BF ; Extended_Pictographic# 7.0 [1] (🂿) PLAYING CARD RED JOKER
1F0C0 ; Extended_Pictographic# NA [1] (🃀) <reserved-1F0C0>
1F0C1..1F0CF ; Extended_Pictographic# 6.0 [15] (🃁..🃏) PLAYING CARD ACE OF DIAMONDS..joker
1F0D0 ; Extended_Pictographic# NA [1] (🃐) <reserved-1F0D0>
1F0D1..1F0DF ; Extended_Pictographic# 6.0 [15] (🃑..🃟) PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER
1F0E0..1F0F5 ; Extended_Pictographic# 7.0 [22] (🃠..🃵) PLAYING CARD FOOL..PLAYING CARD TRUMP-21
1F0F6..1F0FF ; Extended_Pictographic# NA [10] (🃶..🃿) <reserved-1F0F6>..<reserved-1F0FF>
1F10D..1F10F ; Extended_Pictographic# NA [3] (🄍..🄏) <reserved-1F10D>..<reserved-1F10F>
1F12F ; Extended_Pictographic# 11.0 [1] (🄯) COPYLEFT SYMBOL
1F16C ; Extended_Pictographic# 12.0 [1] (🅬) RAISED MR SIGN
1F16D..1F16F ; Extended_Pictographic# NA [3] (🅭..🅯) <reserved-1F16D>..<reserved-1F16F>
1F170..1F171 ; Extended_Pictographic# 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
1F17E ; Extended_Pictographic# 6.0 [1] (🅾️) O button (blood type)
1F17F ; Extended_Pictographic# 5.2 [1] (🅿️) P button
1F18E ; Extended_Pictographic# 6.0 [1] (🆎) AB button (blood type)
1F191..1F19A ; Extended_Pictographic# 6.0 [10] (🆑..🆚) CL button..VS button
1F1AD..1F1E5 ; Extended_Pictographic# NA [57] (🆭..🇥) <reserved-1F1AD>..<reserved-1F1E5>
1F201..1F202 ; Extended_Pictographic# 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
1F203..1F20F ; Extended_Pictographic# NA [13] (🈃..🈏) <reserved-1F203>..<reserved-1F20F>
1F21A ; Extended_Pictographic# 5.2 [1] (🈚) Japanese “free of charge” button
1F22F ; Extended_Pictographic# 5.2 [1] (🈯) Japanese “reserved” button
1F232..1F23A ; Extended_Pictographic# 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
1F23C..1F23F ; Extended_Pictographic# NA [4] (🈼..🈿) <reserved-1F23C>..<reserved-1F23F>
1F249..1F24F ; Extended_Pictographic# NA [7] (🉉..🉏) <reserved-1F249>..<reserved-1F24F>
1F250..1F251 ; Extended_Pictographic# 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
1F252..1F25F ; Extended_Pictographic# NA [14] (🉒..🉟) <reserved-1F252>..<reserved-1F25F>
1F260..1F265 ; Extended_Pictographic# 10.0 [6] (🉠..🉥) ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI
1F266..1F2FF ; Extended_Pictographic# NA[154] (🉦..🋿) <reserved-1F266>..<reserved-1F2FF>
1F300..1F320 ; Extended_Pictographic# 6.0 [33] (🌀..🌠) cyclone..shooting star
1F321..1F32C ; Extended_Pictographic# 7.0 [12] (🌡️..🌬️) thermometer..wind face
1F32D..1F32F ; Extended_Pictographic# 8.0 [3] (🌭..🌯) hot dog..burrito
1F330..1F335 ; Extended_Pictographic# 6.0 [6] (🌰..🌵) chestnut..cactus
1F336 ; Extended_Pictographic# 7.0 [1] (🌶️) hot pepper
1F337..1F37C ; Extended_Pictographic# 6.0 [70] (🌷..🍼) tulip..baby bottle
1F37D ; Extended_Pictographic# 7.0 [1] (🍽️) fork and knife with plate
1F37E..1F37F ; Extended_Pictographic# 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
1F380..1F393 ; Extended_Pictographic# 6.0 [20] (🎀..🎓) ribbon..graduation cap
1F394..1F39F ; Extended_Pictographic# 7.0 [12] (🎔..🎟️) HEART WITH TIP ON THE LEFT..admission tickets
1F3A0..1F3C4 ; Extended_Pictographic# 6.0 [37] (🎠..🏄) carousel horse..person surfing
1F3C5 ; Extended_Pictographic# 7.0 [1] (🏅) sports medal
1F3C6..1F3CA ; Extended_Pictographic# 6.0 [5] (🏆..🏊) trophy..person swimming
1F3CB..1F3CE ; Extended_Pictographic# 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
1F3CF..1F3D3 ; Extended_Pictographic# 8.0 [5] (🏏..🏓) cricket game..ping pong
1F3D4..1F3DF ; Extended_Pictographic# 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
1F3E0..1F3F0 ; Extended_Pictographic# 6.0 [17] (🏠..🏰) house..castle
1F3F1..1F3F7 ; Extended_Pictographic# 7.0 [7] (🏱..🏷️) WHITE PENNANT..label
1F3F8..1F3FA ; Extended_Pictographic# 8.0 [3] (🏸..🏺) badminton..amphora
1F400..1F43E ; Extended_Pictographic# 6.0 [63] (🐀..🐾) rat..paw prints
1F43F ; Extended_Pictographic# 7.0 [1] (🐿️) chipmunk
1F440 ; Extended_Pictographic# 6.0 [1] (👀) eyes
1F441 ; Extended_Pictographic# 7.0 [1] (👁️) eye
1F442..1F4F7 ; Extended_Pictographic# 6.0[182] (👂..📷) ear..camera
1F4F8 ; Extended_Pictographic# 7.0 [1] (📸) camera with flash
1F4F9..1F4FC ; Extended_Pictographic# 6.0 [4] (📹..📼) video camera..videocassette
1F4FD..1F4FE ; Extended_Pictographic# 7.0 [2] (📽️..📾) film projector..PORTABLE STEREO
1F4FF ; Extended_Pictographic# 8.0 [1] (📿) prayer beads
1F500..1F53D ; Extended_Pictographic# 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
1F546..1F54A ; Extended_Pictographic# 7.0 [5] (🕆..🕊️) WHITE LATIN CROSS..dove
1F54B..1F54F ; Extended_Pictographic# 8.0 [5] (🕋..🕏) kaaba..BOWL OF HYGIEIA
1F550..1F567 ; Extended_Pictographic# 6.0 [24] (🕐..🕧) one oclock..twelve-thirty
1F568..1F579 ; Extended_Pictographic# 7.0 [18] (🕨..🕹️) RIGHT SPEAKER..joystick
1F57A ; Extended_Pictographic# 9.0 [1] (🕺) man dancing
1F57B..1F5A3 ; Extended_Pictographic# 7.0 [41] (🕻..🖣) LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX
1F5A4 ; Extended_Pictographic# 9.0 [1] (🖤) black heart
1F5A5..1F5FA ; Extended_Pictographic# 7.0 [86] (🖥️..🗺️) desktop computer..world map
1F5FB..1F5FF ; Extended_Pictographic# 6.0 [5] (🗻..🗿) mount fuji..moai
1F600 ; Extended_Pictographic# 6.1 [1] (😀) grinning face
1F601..1F610 ; Extended_Pictographic# 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
1F611 ; Extended_Pictographic# 6.1 [1] (😑) expressionless face
1F612..1F614 ; Extended_Pictographic# 6.0 [3] (😒..😔) unamused face..pensive face
1F615 ; Extended_Pictographic# 6.1 [1] (😕) confused face
1F616 ; Extended_Pictographic# 6.0 [1] (😖) confounded face
1F617 ; Extended_Pictographic# 6.1 [1] (😗) kissing face
1F618 ; Extended_Pictographic# 6.0 [1] (😘) face blowing a kiss
1F619 ; Extended_Pictographic# 6.1 [1] (😙) kissing face with smiling eyes
1F61A ; Extended_Pictographic# 6.0 [1] (😚) kissing face with closed eyes
1F61B ; Extended_Pictographic# 6.1 [1] (😛) face with tongue
1F61C..1F61E ; Extended_Pictographic# 6.0 [3] (😜..😞) winking face with tongue..disappointed face
1F61F ; Extended_Pictographic# 6.1 [1] (😟) worried face
1F620..1F625 ; Extended_Pictographic# 6.0 [6] (😠..😥) angry face..sad but relieved face
1F626..1F627 ; Extended_Pictographic# 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
1F628..1F62B ; Extended_Pictographic# 6.0 [4] (😨..😫) fearful face..tired face
1F62C ; Extended_Pictographic# 6.1 [1] (😬) grimacing face
1F62D ; Extended_Pictographic# 6.0 [1] (😭) loudly crying face
1F62E..1F62F ; Extended_Pictographic# 6.1 [2] (😮..😯) face with open mouth..hushed face
1F630..1F633 ; Extended_Pictographic# 6.0 [4] (😰..😳) anxious face with sweat..flushed face
1F634 ; Extended_Pictographic# 6.1 [1] (😴) sleeping face
1F635..1F640 ; Extended_Pictographic# 6.0 [12] (😵..🙀) dizzy face..weary cat
1F641..1F642 ; Extended_Pictographic# 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
1F643..1F644 ; Extended_Pictographic# 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
1F645..1F64F ; Extended_Pictographic# 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
1F680..1F6C5 ; Extended_Pictographic# 6.0 [70] (🚀..🛅) rocket..left luggage
1F6C6..1F6CF ; Extended_Pictographic# 7.0 [10] (🛆..🛏️) TRIANGLE WITH ROUNDED CORNERS..bed
1F6D0 ; Extended_Pictographic# 8.0 [1] (🛐) place of worship
1F6D1..1F6D2 ; Extended_Pictographic# 9.0 [2] (🛑..🛒) stop sign..shopping cart
1F6D3..1F6D4 ; Extended_Pictographic# 10.0 [2] (🛓..🛔) STUPA..PAGODA
1F6D5 ; Extended_Pictographic# 12.0 [1] (🛕) hindu temple
1F6D6..1F6DF ; Extended_Pictographic# NA [10] (🛖..🛟) <reserved-1F6D6>..<reserved-1F6DF>
1F6E0..1F6EC ; Extended_Pictographic# 7.0 [13] (🛠️..🛬) hammer and wrench..airplane arrival
1F6ED..1F6EF ; Extended_Pictographic# NA [3] (🛭..🛯) <reserved-1F6ED>..<reserved-1F6EF>
1F6F0..1F6F3 ; Extended_Pictographic# 7.0 [4] (🛰️..🛳️) satellite..passenger ship
1F6F4..1F6F6 ; Extended_Pictographic# 9.0 [3] (🛴..🛶) kick scooter..canoe
1F6F7..1F6F8 ; Extended_Pictographic# 10.0 [2] (🛷..🛸) sled..flying saucer
1F6F9 ; Extended_Pictographic# 11.0 [1] (🛹) skateboard
1F6FA ; Extended_Pictographic# 12.0 [1] (🛺) auto rickshaw
1F6FB..1F6FF ; Extended_Pictographic# NA [5] (🛻..🛿) <reserved-1F6FB>..<reserved-1F6FF>
1F774..1F77F ; Extended_Pictographic# NA [12] (🝴..🝿) <reserved-1F774>..<reserved-1F77F>
1F7D5..1F7D8 ; Extended_Pictographic# 11.0 [4] (🟕..🟘) CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE
1F7D9..1F7DF ; Extended_Pictographic# NA [7] (🟙..🟟) <reserved-1F7D9>..<reserved-1F7DF>
1F7E0..1F7EB ; Extended_Pictographic# 12.0 [12] (🟠..🟫) orange circle..brown square
1F7EC..1F7FF ; Extended_Pictographic# NA [20] (🟬..🟿) <reserved-1F7EC>..<reserved-1F7FF>
1F80C..1F80F ; Extended_Pictographic# NA [4] (🠌..🠏) <reserved-1F80C>..<reserved-1F80F>
1F848..1F84F ; Extended_Pictographic# NA [8] (🡈..🡏) <reserved-1F848>..<reserved-1F84F>
1F85A..1F85F ; Extended_Pictographic# NA [6] (🡚..🡟) <reserved-1F85A>..<reserved-1F85F>
1F888..1F88F ; Extended_Pictographic# NA [8] (🢈..🢏) <reserved-1F888>..<reserved-1F88F>
1F8AE..1F8FF ; Extended_Pictographic# NA [82] (🢮..🣿) <reserved-1F8AE>..<reserved-1F8FF>
1F90C ; Extended_Pictographic# NA [1] (🤌) <reserved-1F90C>
1F90D..1F90F ; Extended_Pictographic# 12.0 [3] (🤍..🤏) white heart..pinching hand
1F910..1F918 ; Extended_Pictographic# 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
1F919..1F91E ; Extended_Pictographic# 9.0 [6] (🤙..🤞) call me hand..crossed fingers
1F91F ; Extended_Pictographic# 10.0 [1] (🤟) love-you gesture
1F920..1F927 ; Extended_Pictographic# 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
1F928..1F92F ; Extended_Pictographic# 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
1F930 ; Extended_Pictographic# 9.0 [1] (🤰) pregnant woman
1F931..1F932 ; Extended_Pictographic# 10.0 [2] (🤱..🤲) breast-feeding..palms up together
1F933..1F93A ; Extended_Pictographic# 9.0 [8] (🤳..🤺) selfie..person fencing
1F93C..1F93E ; Extended_Pictographic# 9.0 [3] (🤼..🤾) people wrestling..person playing handball
1F93F ; Extended_Pictographic# 12.0 [1] (🤿) diving mask
1F940..1F945 ; Extended_Pictographic# 9.0 [6] (🥀..🥅) wilted flower..goal net
1F947..1F94B ; Extended_Pictographic# 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
1F94C ; Extended_Pictographic# 10.0 [1] (🥌) curling stone
1F94D..1F94F ; Extended_Pictographic# 11.0 [3] (🥍..🥏) lacrosse..flying disc
1F950..1F95E ; Extended_Pictographic# 9.0 [15] (🥐..🥞) croissant..pancakes
1F95F..1F96B ; Extended_Pictographic# 10.0 [13] (🥟..🥫) dumpling..canned food
1F96C..1F970 ; Extended_Pictographic# 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
1F971 ; Extended_Pictographic# 12.0 [1] (🥱) yawning face
1F972 ; Extended_Pictographic# NA [1] (🥲) <reserved-1F972>
1F973..1F976 ; Extended_Pictographic# 11.0 [4] (🥳..🥶) partying face..cold face
1F977..1F979 ; Extended_Pictographic# NA [3] (🥷..🥹) <reserved-1F977>..<reserved-1F979>
1F97A ; Extended_Pictographic# 11.0 [1] (🥺) pleading face
1F97B ; Extended_Pictographic# 12.0 [1] (🥻) sari
1F97C..1F97F ; Extended_Pictographic# 11.0 [4] (🥼..🥿) lab coat..flat shoe
1F980..1F984 ; Extended_Pictographic# 8.0 [5] (🦀..🦄) crab..unicorn
1F985..1F991 ; Extended_Pictographic# 9.0 [13] (🦅..🦑) eagle..squid
1F992..1F997 ; Extended_Pictographic# 10.0 [6] (🦒..🦗) giraffe..cricket
1F998..1F9A2 ; Extended_Pictographic# 11.0 [11] (🦘..🦢) kangaroo..swan
1F9A3..1F9A4 ; Extended_Pictographic# NA [2] (🦣..🦤) <reserved-1F9A3>..<reserved-1F9A4>
1F9A5..1F9AA ; Extended_Pictographic# 12.0 [6] (🦥..🦪) sloth..oyster
1F9AB..1F9AD ; Extended_Pictographic# NA [3] (🦫..🦭) <reserved-1F9AB>..<reserved-1F9AD>
1F9AE..1F9AF ; Extended_Pictographic# 12.0 [2] (🦮..🦯) guide dog..probing cane
1F9B0..1F9B9 ; Extended_Pictographic# 11.0 [10] (🦰..🦹) red hair..supervillain
1F9BA..1F9BF ; Extended_Pictographic# 12.0 [6] (🦺..🦿) safety vest..mechanical leg
1F9C0 ; Extended_Pictographic# 8.0 [1] (🧀) cheese wedge
1F9C1..1F9C2 ; Extended_Pictographic# 11.0 [2] (🧁..🧂) cupcake..salt
1F9C3..1F9CA ; Extended_Pictographic# 12.0 [8] (🧃..🧊) beverage box..ice cube
1F9CB..1F9CC ; Extended_Pictographic# NA [2] (🧋..🧌) <reserved-1F9CB>..<reserved-1F9CC>
1F9CD..1F9CF ; Extended_Pictographic# 12.0 [3] (🧍..🧏) person standing..deaf person
1F9D0..1F9E6 ; Extended_Pictographic# 10.0 [23] (🧐..🧦) face with monocle..socks
1F9E7..1F9FF ; Extended_Pictographic# 11.0 [25] (🧧..🧿) red envelope..nazar amulet
1FA00..1FA53 ; Extended_Pictographic# 12.0 [84] (🨀..🩓) NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP
1FA54..1FA5F ; Extended_Pictographic# NA [12] (🩔..🩟) <reserved-1FA54>..<reserved-1FA5F>
1FA60..1FA6D ; Extended_Pictographic# 11.0 [14] (🩠..🩭) XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER
1FA6E..1FA6F ; Extended_Pictographic# NA [2] (🩮..🩯) <reserved-1FA6E>..<reserved-1FA6F>
1FA70..1FA73 ; Extended_Pictographic# 12.0 [4] (🩰..🩳) ballet shoes..shorts
1FA74..1FA77 ; Extended_Pictographic# NA [4] (🩴..🩷) <reserved-1FA74>..<reserved-1FA77>
1FA78..1FA7A ; Extended_Pictographic# 12.0 [3] (🩸..🩺) drop of blood..stethoscope
1FA7B..1FA7F ; Extended_Pictographic# NA [5] (🩻..🩿) <reserved-1FA7B>..<reserved-1FA7F>
1FA80..1FA82 ; Extended_Pictographic# 12.0 [3] (🪀..🪂) yo-yo..parachute
1FA83..1FA8F ; Extended_Pictographic# NA [13] (🪃..🪏) <reserved-1FA83>..<reserved-1FA8F>
1FA90..1FA95 ; Extended_Pictographic# 12.0 [6] (🪐..🪕) ringed planet..banjo
1FA96..1FFFD ; Extended_Pictographic# NA[1384] (🪖..🿽) <reserved-1FA96>..<reserved-1FFFD>
# Total elements: 3793
#EOF

View file

@ -98,4 +98,35 @@ def code_change(_old_vsn, state, _extra) do
defp update_emojis(emojis) do defp update_emojis(emojis) do
:ets.insert(@ets, emojis) :ets.insert(@ets, emojis)
end end
@external_resource "lib/pleroma/emoji-data.txt"
emojis =
@external_resource
|> File.read!()
|> String.split("\n")
|> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end)
|> Enum.map(fn line ->
line
|> String.split(";", parts: 2)
|> hd()
|> String.trim()
|> String.split("..")
|> case do
[number] ->
<<String.to_integer(number, 16)::utf8>>
[first, last] ->
String.to_integer(first, 16)..String.to_integer(last, 16)
|> Enum.map(&<<&1::utf8>>)
end
end)
|> List.flatten()
|> Enum.uniq()
for emoji <- emojis do
def is_unicode_emoji?(unquote(emoji)), do: true
end
def is_unicode_emoji?(_), do: false
end end

View file

@ -64,6 +64,8 @@ def contain_origin(id, %{"actor" => _actor} = params) do
def contain_origin(id, %{"attributedTo" => actor} = params), def contain_origin(id, %{"attributedTo" => actor} = params),
do: contain_origin(id, Map.put(params, "actor", actor)) do: contain_origin(id, Map.put(params, "actor", actor))
def contain_origin(_id, _data), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
id_uri = URI.parse(id) id_uri = URI.parse(id)
other_uri = URI.parse(other_id) other_uri = URI.parse(other_id)

View file

@ -16,14 +16,28 @@ def secret_token do
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"admin_token" => admin_token}} = conn, _) do def call(conn, _) do
if secret_token() && admin_token == secret_token() do if secret_token() do
conn authenticate(conn)
|> assign(:user, %User{is_admin: true})
else else
conn conn
end end
end end
def call(conn, _), do: conn def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
assign(conn, :user, %User{is_admin: true})
else
conn
end
end
def authenticate(conn) do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
[^token] -> assign(conn, :user, %User{is_admin: true})
_ -> conn
end
end
end end

View file

@ -71,7 +71,7 @@ defp fetch_user_and_token(token) do
) )
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
with %Token{user: %{deactivated: false} = user} = token_record <- Repo.one(query) do with %Token{user: user} = token_record <- Repo.one(query) do
{:ok, user, token_record} {:ok, user, token_record}
end end
end end

View file

@ -10,9 +10,13 @@ def init(options) do
options options
end end
def call(%{assigns: %{user: %User{deactivated: true}}} = conn, _) do def call(%{assigns: %{user: %User{} = user}} = conn, _) do
conn if User.auth_active?(user) do
|> assign(:user, nil) conn
else
conn
|> assign(:user, nil)
end
end end
def call(conn, _) do def call(conn, _) do

View file

@ -124,6 +124,9 @@ defmodule Pleroma.User do
timestamps() timestamps()
end end
@doc "Returns if the user should be allowed to authenticate"
def auth_active?(%User{deactivated: true}), do: false
def auth_active?(%User{confirmation_pending: true}), def auth_active?(%User{confirmation_pending: true}),
do: !Pleroma.Config.get([:instance, :account_activation_required]) do: !Pleroma.Config.get([:instance, :account_activation_required])

View file

@ -322,6 +322,32 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end end
end end
def react_with_emoji(user, object, emoji, options \\ []) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
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}
end
end
def 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}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like( def like(
%User{ap_id: ap_id} = user, %User{ap_id: ap_id} = user,

View file

@ -0,0 +1,101 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
require Pleroma.Constants
@moduledoc "Filter activities depending on their age"
@behaviour MRF
defp check_date(%{"published" => published} = message) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
{:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
{:ok, message}
else
{:ttl, true} ->
{:reject, nil}
e ->
{:error, e}
end
end
defp check_reject(message, actions) do
if :reject in actions do
{:reject, nil}
else
{:ok, message}
end
end
defp check_delist(message, actions) do
if :delist in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
end
else
{:ok, message}
end
end
defp check_strip_followers(message, actions) do
if :strip_followers in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
to = List.delete(message["to"], user.follower_address)
cc = List.delete(message["cc"], user.follower_address)
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
end
else
{:ok, message}
end
end
@impl true
def filter(%{"type" => "Create", "published" => _} = message) do
with actions <- Config.get([:mrf_object_age, :actions]),
{:reject, _} <- check_date(message),
{:ok, message} <- check_reject(message, actions),
{:ok, message} <- check_delist(message, actions),
{:ok, message} <- check_strip_followers(message, actions) do
{:ok, message}
else
# check_date() is allowed to short-circuit the pipeline
e -> e
end
end
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -566,6 +566,34 @@ def handle_incoming(
end end
end end
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
"laugh" => "😆",
"hmm" => "🤔",
"surprise" => "😮",
"congrats" => "🎉",
"angry" => "💢",
"confused" => "😥",
"rip" => "😇",
"pudding" => "🍮",
"star" => ""
}
@doc "Rewrite misskey likes into EmojiReactions"
def handle_incoming(
%{
"type" => "Like",
"_misskey_reaction" => reaction
} = data,
options
) do
data
|> Map.put("type", "EmojiReaction")
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|> handle_incoming(options)
end
def handle_incoming( def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data, %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options _options
@ -580,6 +608,27 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{
"type" => "EmojiReaction",
"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
@ -715,6 +764,28 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "EmojiReaction", "id" => reaction_activity_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, activity, _} <-
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
activity_id: id,
local: false
) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Undo", "type" => "Undo",
@ -1048,7 +1119,7 @@ def prepare_attachments(object) do
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
end end
defp strip_internal_fields(object) do def strip_internal_fields(object) do
object object
|> Map.drop(Pleroma.Constants.object_internal_fields()) |> Map.drop(Pleroma.Constants.object_internal_fields())
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
@ -255,6 +256,16 @@ def get_existing_like(actor, %{data: %{"id" => id}}) do
|> Repo.one() |> Repo.one()
end end
@doc """
Returns like activities targeting an object
"""
def get_object_likes(%{data: %{"id" => id}}) do
id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Like")
|> Repo.all()
end
@spec make_like_data(User.t(), map(), String.t()) :: map() @spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data( def make_like_data(
%User{ap_id: ap_id} = actor, %User{ap_id: ap_id} = actor,
@ -286,13 +297,30 @@ def make_like_data(
|> maybe_put("id", activity_id) |> maybe_put("id", activity_id)
end end
def make_emoji_reaction_data(user, object, emoji, activity_id) do
make_like_data(user, object, activity_id)
|> Map.put("type", "EmojiReaction")
|> Map.put("content", emoji)
end
@spec update_element_in_object(String.t(), list(any), Object.t()) :: @spec update_element_in_object(String.t(), list(any), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do def update_element_in_object(property, element, object) do
length =
if is_map(element) do
element
|> Map.values()
|> List.flatten()
|> length()
else
element
|> length()
end
data = data =
Map.merge( Map.merge(
object.data, object.data,
%{"#{property}_count" => length(element), "#{property}s" => element} %{"#{property}_count" => length, "#{property}s" => element}
) )
object object
@ -300,6 +328,38 @@ def update_element_in_object(property, element, object) do
|> Object.update_and_set_cache() |> Object.update_and_set_cache()
end end
@spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}},
object
) do
reactions = object.data["reactions"] || %{}
emoji_actors = reactions[emoji] || []
new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
new_reactions = Map.put(reactions, emoji, new_emoji_actors)
update_element_in_object("reaction", new_reactions, object)
end
def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}},
object
) do
reactions = object.data["reactions"] || %{}
emoji_actors = reactions[emoji] || []
new_emoji_actors = List.delete(emoji_actors, actor)
new_reactions =
if new_emoji_actors == [] do
Map.delete(reactions, emoji)
else
Map.put(reactions, emoji, new_emoji_actors)
end
update_element_in_object("reaction", new_reactions, object)
end
@spec add_like_to_object(Activity.t(), Object.t()) :: @spec add_like_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
@ -397,6 +457,19 @@ def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
|> Repo.one() |> Repo.one()
end end
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
"EmojiReaction"
|> Activity.Queries.by_type()
|> where(actor: ^ap_id)
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|> Activity.Queries.by_object_id(object_ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
#### Announce-related helpers #### Announce-related helpers
@doc """ @doc """
@ -489,6 +562,25 @@ def make_unlike_data(
|> maybe_put("id", activity_id) |> maybe_put("id", activity_id)
end end
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
data: %{"id" => undone_activity_id, "context" => context},
actor: undone_activity_actor
},
activity_id \\ nil
) do
%{
"type" => "Undo",
"actor" => actor,
"object" => undone_activity_id,
"to" => [follower_address, undone_activity_actor],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) :: @spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object( def add_announce_to_object(
@ -615,26 +707,31 @@ def make_flag_data(%{actor: actor, context: context, content: content} = params,
def make_flag_data(_, _), do: %{} def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses} = _) do defp build_flag_object(%{account: account, statuses: statuses} = _) do
[account.ap_id] ++ [account.ap_id] ++ build_flag_object(%{statuses: statuses})
Enum.map(statuses || [], fn act -> end
id =
case act do
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end
activity = Activity.get_by_ap_id_with_object(id) defp build_flag_object(%{statuses: statuses}) do
actor = User.get_by_ap_id(activity.object.data["actor"]) Enum.map(statuses || [], &build_flag_object/1)
end
%{ defp build_flag_object(act) when is_map(act) or is_binary(act) do
"type" => "Note", id =
"id" => activity.data["id"], case act do
"content" => activity.object.data["content"], %Activity{} = act -> act.data["id"]
"published" => activity.object.data["published"], act when is_map(act) -> act["id"]
"actor" => AccountView.render("show.json", %{user: actor}) act when is_binary(act) -> act
} end
end)
activity = Activity.get_by_ap_id_with_object(id)
actor = User.get_by_ap_id(activity.object.data["actor"])
%{
"type" => "Note",
"id" => activity.data["id"],
"content" => activity.object.data["content"],
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: actor})
}
end end
defp build_flag_object(_), do: [] defp build_flag_object(_), do: []
@ -679,6 +776,94 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
end end
#### Report-related helpers #### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset)
end
@spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
required(:groups) => [
%{
required(:date) => String.t(),
required(:account) => %{},
required(:status) => %{},
required(:actors) => [%User{}],
required(:reports) => [%Activity{}]
}
],
required(:total) => integer
}
def get_reports_grouped_by_status(groups) do
parsed_groups =
groups
|> Enum.map(fn entry ->
activity =
case Jason.decode(entry.activity) do
{:ok, activity} -> activity
_ -> build_flag_object(entry.activity)
end
parse_report_group(activity)
end)
%{
groups: parsed_groups
}
end
def parse_report_group(activity) do
reports = get_reports_by_status_id(activity["id"])
max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
actors = Enum.map(reports, & &1.user_actor)
%{
date: max_date.data["published"],
account: activity["actor"],
status: %{
id: activity["id"],
content: activity["content"],
published: activity["published"]
},
actors: Enum.uniq(actors),
reports: reports
}
end
def get_reports_by_status_id(ap_id) do
from(a in Activity,
where: fragment("(?)->>'type' = 'Flag'", a.data),
where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}])
)
|> Activity.with_preloaded_user_actor()
|> Repo.all()
end
@spec get_reported_activities() :: [
%{
required(:activity) => String.t(),
required(:date) => String.t()
}
]
def get_reported_activities do
from(a in Activity,
where: fragment("(?)->>'type' = 'Flag'", a.data),
select: %{
date: fragment("max(?->>'published') date", a.data),
activity:
fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
},
group_by: fragment("activity"),
order_by: fragment("date DESC")
)
|> Repo.all()
end
def update_report_state(%Activity{} = activity, state) def update_report_state(%Activity{} = activity, state)
when state in @strip_status_report_states do when state in @strip_status_report_states do
@ -702,6 +887,18 @@ def update_report_state(%Activity{} = activity, state) when state in @supported_
|> Repo.update() |> Repo.update()
end end
def update_report_state(activity_ids, state) when state in @supported_report_states do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"} def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do def strip_report_status_data(activity) do

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
@ -624,19 +625,17 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
params = conn
params |> put_view(ReportView)
|> Map.put("type", "Flag") |> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
|> Map.put("skip_preload", true) end
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
reports = ActivityPub.fetch_activities([], params, :offset) def list_grouped_reports(conn, _params) do
reports = Utils.get_reported_activities()
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("index.json", %{reports: reports}) |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
end end
def report_show(conn, %{"id" => id}) do def report_show(conn, %{"id" => id}) do
@ -649,17 +648,26 @@ def report_show(conn, %{"id" => id}) do
end end
end end
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do result =
ModerationLog.insert_log(%{ reports
action: "report_update", |> Enum.map(fn report ->
actor: admin, with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
subject: report ModerationLog.insert_log(%{
}) action: "report_update",
actor: admin,
subject: activity
})
conn activity
|> put_view(ReportView) else
|> render("show.json", Report.extract_report_info(report)) {:error, message} -> %{id: report["id"], error: message}
end
end)
case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end end
end end

View file

@ -42,6 +42,26 @@ def render("show.json", %{report: report, user: user, account: account, statuses
} }
end end
def render("index_grouped.json", %{groups: groups}) do
reports =
Enum.map(groups, fn group ->
%{
date: group[:date],
account: group[:account],
status: group[:status],
actors: Enum.map(group[:actors], &merge_account_views/1),
reports:
group[:reports]
|> Enum.map(&Report.extract_report_info(&1))
|> Enum.map(&render(__MODULE__, "show.json", &1))
}
end)
%{
reports: reports
}
end
defp merge_account_views(%User{} = user) do defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))

View file

@ -120,6 +120,25 @@ def unfavorite(id_or_ap_id, user) do
end end
end end
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity) do
ActivityPub.react_with_emoji(user, object, emoji)
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
end
end
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
else
_ ->
{:error, dgettext("errors", "Could not remove reaction emoji")}
end
end
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user), with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object), :ok <- validate_existing_votes(user, object),
@ -351,6 +370,13 @@ defp get_reported_account(account_id) do
end end
end end
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_report_state(activity_id, state) do def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state) Utils.update_report_state(activity, state)

View file

@ -12,7 +12,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("participations.json", %{participations: participations, for: user}) do def render("participations.json", %{participations: participations, for: user}) do
render_many(participations, __MODULE__, "participation.json", as: :participation, for: user) safe_render_many(participations, __MODULE__, "participation.json", %{
as: :participation,
for: user
})
end end
def render("participation.json", %{participation: participation, for: user}) do def render("participation.json", %{participation: participation, for: user}) do

View file

@ -120,6 +120,12 @@ def raw_nodeinfo do
banner: Config.get([:instance, :banner_upload_limit]), banner: Config.get([:instance, :banner_upload_limit]),
background: Config.get([:instance, :background_upload_limit]) background: Config.get([:instance, :background_upload_limit])
}, },
fieldsLimits: %{
maxFields: Config.get([:instance, :max_account_fields]),
maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
nameLength: Config.get([:instance, :account_field_name_length]),
valueLength: Config.get([:instance, :account_field_value_length])
},
accountActivationRequired: Config.get([:instance, :account_activation_required], false), accountActivationRequired: Config.get([:instance, :account_activation_required], false),
invitesEnabled: Config.get([:instance, :invites_enabled], false), invitesEnabled: Config.get([:instance, :invites_enabled], false),
mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),

View file

@ -7,10 +7,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Activity
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@ -29,6 +34,47 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => emoji_reactions}} <- Object.normalize(activity) do
reactions =
emoji_reactions
|> Enum.map(fn {emoji, users} ->
users = Enum.map(users, &User.get_cached_by_ap_id/1)
{emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})}
end)
|> Enum.into(%{})
conn
|> json(reactions)
else
_e ->
conn
|> json(%{})
end
end
def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity, for: user, as: :activity})
end
end
def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
"id" => activity_id,
"emoji" => emoji
}) do
with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity, for: user, as: :activity})
end
end
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id), with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do true <- user.id == participation.user_id do

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.Router do
pipeline :oauth do pipeline :oauth do
plug(:fetch_session) plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug) plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
end end
pipeline :api do pipeline :api do
@ -178,8 +179,9 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/reports", AdminAPIController, :list_reports) get("/reports", AdminAPIController, :list_reports)
get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", AdminAPIController, :report_show)
put("/reports/:id", AdminAPIController, :report_update_state) patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/respond", AdminAPIController, :report_respond) post("/reports/:id/respond", AdminAPIController, :report_respond)
put("/statuses/:id", AdminAPIController, :status_update) put("/statuses/:id", AdminAPIController, :status_update)
@ -260,6 +262,12 @@ defmodule Pleroma.Web.Router do
end end
end end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/statuses/:id/emoji_reactions_by", PleromaAPIController, :emoji_reactions_by)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do scope [] do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
@ -273,6 +281,8 @@ defmodule Pleroma.Web.Router do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
patch("/conversations/:id", PleromaAPIController, :update_conversation) patch("/conversations/:id", PleromaAPIController, :update_conversation)
post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji)
post("/statuses/:id/unreact_with_emoji", PleromaAPIController, :unreact_with_emoji)
post("/notifications/read", PleromaAPIController, :read_notification) post("/notifications/read", PleromaAPIController, :read_notification)
patch("/accounts/update_avatar", AccountController, :update_avatar) patch("/accounts/update_avatar", AccountController, :update_avatar)

View file

@ -27,6 +27,12 @@ defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
defp get_title(_), do: nil defp get_title(_), do: nil
defp not_found(conn, message) do
conn
|> put_status(404)
|> render("error.html", %{message: message, meta: ""})
end
def get_counts(%Activity{} = activity) do def get_counts(%Activity{} = activity) do
%Object{data: data} = Object.normalize(activity) %Object{data: data} = Object.normalize(activity)
@ -77,10 +83,13 @@ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
render(conn, "conversation.html", %{activities: timeline, meta: meta}) render(conn, "conversation.html", %{activities: timeline, meta: meta})
else else
_ -> %Activity{object: %Object{data: data}} ->
conn conn
|> put_status(404) |> put_status(:found)
|> render("error.html", %{message: "Post not found.", meta: ""}) |> redirect(external: data["url"] || data["external_url"] || data["id"])
_ ->
not_found(conn, "Post not found.")
end end
end end
@ -108,9 +117,33 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
}) })
_ -> _ ->
conn not_found(conn, "User not found.")
|> put_status(404) end
|> render("error.html", %{message: "User not found.", meta: ""}) end
def show(%{assigns: %{object_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_create_by_object_ap_id_with_object(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{activity_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_by_ap_id(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end end
end end
@ -120,5 +153,11 @@ def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
def assign_id(%{path_info: ["users", user_id]} = conn, _opts), def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id) do: assign(conn, :username_or_id, user_id)
def assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
do: assign(conn, :object_id, object_id)
def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
do: assign(conn, :activity_id, activity_id)
def assign_id(conn, _opts), do: conn def assign_id(conn, _opts), do: conn
end end

View file

@ -35,8 +35,8 @@
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
"ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
"excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"}, "fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"},
"fast_sanitize": {:hex, :fast_sanitize, "0.1.3", "e89a743b1679c344abdfcf79778d1499fbc599eca2d8a8cdfaf9ff520986fb72", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"},

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link rel=stylesheet href=/static/font/css/lato.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.5c3fab032deb5f2793cb.js></script><script type=text/javascript src=/static/js/app.04db7997189e10d53373.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link rel=stylesheet href=/static/font/css/lato.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.76db8e4cdf29decd5cab.js></script><script type=text/javascript src=/static/js/app.b2fc47df23c3df3426bd.js></script></body></html>

View file

@ -28,7 +28,8 @@
"oauthRegistrationEndpoint": { "oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint", "@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id" "@type": "@id"
} },
"EmojiReaction": "litepub:EmojiReaction"
} }
] ]
} }

View file

@ -1,2 +1,2 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{1012:function(t,e,i){"use strict";i.r(e);var n=i(1013),c=i.n(n);for(var r in n)"default"!==r&&function(t){i.d(e,t,function(){return n[t]})}(r);var a=i(1016),s=i(0);var o=function(t){i(1014)},u=Object(s.a)(c.a,a.a,a.b,!1,o,null,null);e.default=u.exports},1013:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=c(i(348));function c(t){return t&&t.__esModule?t:{default:t}}var r={components:{TabSwitcher:c(i(205)).default},data:function(){return{meta:{stickers:[]},path:""}},computed:{pack:function(){return this.$store.state.instance.stickers||[]}},methods:{clear:function(){this.meta={stickers:[]}},pick:function(t,e){var i=this,c=this.$store;fetch(t).then(function(t){t.blob().then(function(t){var r=new File([t],e,{mimetype:"image/png"}),a=new FormData;a.append("file",r),n.default.uploadMedia({store:c,formData:a}).then(function(t){i.$emit("uploaded",t),i.clear()},function(t){console.warn("Can't attach sticker"),console.warn(t),i.$emit("upload-failed","default")})})})}}};e.default=r},1014:function(t,e,i){var n=i(1015);"string"==typeof n&&(n=[[t.i,n,""]]),n.locals&&(t.exports=n.locals);(0,i(2).default)("cc6cdea4",n,!0,{})},1015:function(t,e,i){(t.exports=i(1)(!1)).push([t.i,".sticker-picker{width:100%;position:relative}.sticker-picker .tab-switcher{position:absolute;top:0;bottom:0;left:0;right:0}.sticker-picker .sticker-picker-content .sticker{display:inline-block;width:20%;height:20%}.sticker-picker .sticker-picker-content .sticker img{width:100%}.sticker-picker .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--link,#d8a070))}",""])},1016:function(t,e,i){"use strict";i.d(e,"a",function(){return n}),i.d(e,"b",function(){return c});var n=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"sticker-picker"},[i("tab-switcher",{staticClass:"tab-switcher",attrs:{"render-only-focused":!0,"scrollable-tabs":""}},t._l(t.pack,function(e){return i("div",{key:e.path,staticClass:"sticker-picker-content",attrs:{"image-tooltip":e.meta.title,image:e.path+e.meta.tabIcon}},t._l(e.meta.stickers,function(n){return i("div",{key:n,staticClass:"sticker",on:{click:function(i){i.stopPropagation(),i.preventDefault(),t.pick(e.path+n,e.meta.title)}}},[i("img",{attrs:{src:e.path+n}})])}),0)}),0)],1)},c=[]}}]); (window.webpackJsonp=window.webpackJsonp||[]).push([[2],{1023:function(t,e,i){"use strict";i.r(e);var n=i(1024),c=i.n(n);for(var r in n)"default"!==r&&function(t){i.d(e,t,function(){return n[t]})}(r);var a=i(1027),s=i(0);var o=function(t){i(1025)},u=Object(s.a)(c.a,a.a,a.b,!1,o,null,null);e.default=u.exports},1024:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=c(i(352));function c(t){return t&&t.__esModule?t:{default:t}}var r={components:{TabSwitcher:c(i(207)).default},data:function(){return{meta:{stickers:[]},path:""}},computed:{pack:function(){return this.$store.state.instance.stickers||[]}},methods:{clear:function(){this.meta={stickers:[]}},pick:function(t,e){var i=this,c=this.$store;fetch(t).then(function(t){t.blob().then(function(t){var r=new File([t],e,{mimetype:"image/png"}),a=new FormData;a.append("file",r),n.default.uploadMedia({store:c,formData:a}).then(function(t){i.$emit("uploaded",t),i.clear()},function(t){console.warn("Can't attach sticker"),console.warn(t),i.$emit("upload-failed","default")})})})}}};e.default=r},1025:function(t,e,i){var n=i(1026);"string"==typeof n&&(n=[[t.i,n,""]]),n.locals&&(t.exports=n.locals);(0,i(2).default)("cc6cdea4",n,!0,{})},1026:function(t,e,i){(t.exports=i(1)(!1)).push([t.i,".sticker-picker{width:100%;position:relative}.sticker-picker .tab-switcher{position:absolute;top:0;bottom:0;left:0;right:0}.sticker-picker .sticker-picker-content .sticker{display:inline-block;width:20%;height:20%}.sticker-picker .sticker-picker-content .sticker img{width:100%}.sticker-picker .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--link,#d8a070))}",""])},1027:function(t,e,i){"use strict";i.d(e,"a",function(){return n}),i.d(e,"b",function(){return c});var n=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"sticker-picker"},[i("tab-switcher",{staticClass:"tab-switcher",attrs:{"render-only-focused":!0,"scrollable-tabs":""}},t._l(t.pack,function(e){return i("div",{key:e.path,staticClass:"sticker-picker-content",attrs:{"image-tooltip":e.meta.title,image:e.path+e.meta.tabIcon}},t._l(e.meta.stickers,function(n){return i("div",{key:n,staticClass:"sticker",on:{click:function(i){i.stopPropagation(),i.preventDefault(),t.pick(e.path+n,e.meta.title)}}},[i("img",{attrs:{src:e.path+n}})])}),0)}),0)],1)},c=[]}}]);
//# sourceMappingURL=2.73375b727cef616c59b4.js.map //# sourceMappingURL=2.c96b30ae9f2d3f46f0ad.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
<h4>Terms of Service</h4> <h4>Terms of Service</h4>
<p>Nothing super complex, just a few things</p> <p>I'm pretty lax provided you're nice</p>
<ol> <ol>
<li> <li>

View file

@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/img/nsfw.ca95de9.png","/static/css/app.fd71461124f3eb029b1b.css","/static/js/app.04db7997189e10d53373.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.5c3fab032deb5f2793cb.js","/static/js/2.73375b727cef616c59b4.js"]}; var serviceWorkerOption = {"assets":["/static/img/nsfw.ca95de9.png","/static/css/app.fd71461124f3eb029b1b.css","/static/js/app.b2fc47df23c3df3426bd.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.76db8e4cdf29decd5cab.js","/static/js/2.c96b30ae9f2d3f46f0ad.js"]};
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=0)}([function(e,n,t){"use strict";var r,o=t(1),i=(r=o)&&r.__esModule?r:{default:r};function a(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){return"window"===e.type})})}self.addEventListener("push",function(e){e.data&&e.waitUntil(i.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications}).then(function(n){return n&&a().then(function(n){var t=e.data.json();if(0===n.length)return self.registration.showNotification(t.title,t)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(a().then(function(e){for(var n=0;n<e.length;n++){var t=e[n];if("/"===t.url&&"focus"in t)return t.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,n){ !function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=0)}([function(e,n,t){"use strict";var r,o=t(1),i=(r=o)&&r.__esModule?r:{default:r};function a(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){return"window"===e.type})})}self.addEventListener("push",function(e){e.data&&e.waitUntil(i.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications}).then(function(n){return n&&a().then(function(n){var t=e.data.json();if(0===n.length)return self.registration.showNotification(t.title,t)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(a().then(function(e){for(var n=0;n<e.length;n++){var t=e[n];if("/"===t.url&&"focus"in t)return t.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,n){
/*! /*!

View file

@ -6,6 +6,14 @@ defmodule Pleroma.EmojiTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Pleroma.Emoji alias Pleroma.Emoji
describe "is_unicode_emoji?/1" do
test "tells if a string is an unicode emoji" do
refute Emoji.is_unicode_emoji?("X")
assert Emoji.is_unicode_emoji?("")
assert Emoji.is_unicode_emoji?("🥺")
end
end
describe "get_all/0" do describe "get_all/0" do
setup do setup do
emoji_list = Emoji.get_all() emoji_list = Emoji.get_all()

30
test/fixtures/emoji-reaction.json vendored Normal file
View file

@ -0,0 +1,30 @@
{
"type": "EmojiReaction",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==",
"creator": "http://mastodon.example.org/users/admin#main-key",
"created": "2018-02-17T18:57:49Z"
},
"object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454",
"content": "👌",
"nickname": "lain",
"id": "http://mastodon.example.org/users/admin#reactions/2",
"actor": "http://mastodon.example.org/users/admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"sensitive": "as:sensitive",
"ostatus": "http://ostatus.org#",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"atomUri": "ostatus:atomUri",
"Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji"
}
]
}

14
test/fixtures/misskey-like.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
"@context" : [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{"Hashtag" : "as:Hashtag"}
],
"_misskey_reaction" : "pudding",
"actor": "http://mastodon.example.org/users/admin",
"cc" : ["https://testing.pleroma.lol/users/lain"],
"id" : "https://misskey.xyz/75149198-2f45-46e4-930a-8b0538297075",
"nickname" : "lain",
"object" : "https://testing.pleroma.lol/objects/c331bbf7-2eb9-4801-a709-2a6103492a5a",
"type" : "Like"
}

View file

@ -17,6 +17,16 @@ defmodule Pleroma.Object.ContainmentTest do
end end
describe "general origin containment" do describe "general origin containment" do
test "works for completely actorless posts" do
assert :error ==
Containment.contain_origin("https://glaceon.social/users/monorail", %{
"deleted" => "2019-10-30T05:48:50.249606Z",
"formerType" => "Note",
"id" => "https://glaceon.social/users/monorail/statuses/103049757364029187",
"type" => "Tombstone"
})
end
test "contain_origin_from_id() catches obvious spoofing attempts" do test "contain_origin_from_id() catches obvious spoofing attempts" do
data = %{ data = %{
"id" => "http://example.com/~alyssa/activities/1234.json" "id" => "http://example.com/~alyssa/activities/1234.json"

View file

@ -22,21 +22,39 @@ test "does nothing if a user is assigned", %{conn: conn} do
assert conn == ret_conn assert conn == ret_conn
end end
test "with secret set and given in the 'admin_token' parameter, it assigns an admin user", %{ describe "when secret set it assigns an admin user" do
conn: conn test "with `admin_token` query parameter", %{conn: conn} do
} do Pleroma.Config.put(:admin_token, "password123")
Pleroma.Config.put(:admin_token, "password123")
conn = conn =
%{conn | params: %{"admin_token" => "wrong_password"}} %{conn | params: %{"admin_token" => "wrong_password"}}
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
refute conn.assigns[:user] refute conn.assigns[:user]
conn = conn =
%{conn | params: %{"admin_token" => "password123"}} %{conn | params: %{"admin_token" => "password123"}}
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin assert conn.assigns[:user].is_admin
end
test "with `x-admin-token` HTTP header", %{conn: conn} do
Pleroma.Config.put(:admin_token, "☕️")
conn =
conn
|> put_req_header("x-admin-token", "🥛")
|> AdminSecretAuthenticationPlug.call(%{})
refute conn.assigns[:user]
conn =
conn
|> put_req_header("x-admin-token", "☕️")
|> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin
end
end end
end end

View file

@ -25,7 +25,7 @@ test "config is required for plug to work" do
test "it restricts based on config values" do test "it restricts based on config values" do
limiter_name = :test_opts limiter_name = :test_opts
scale = 60 scale = 80
limit = 5 limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})

View file

@ -16,6 +16,23 @@ test "doesn't do anything if the user isn't set", %{conn: conn} do
assert ret_conn == conn assert ret_conn == conn
end end
test "with a user that's not confirmed and a config requiring confirmation, it removes that user",
%{conn: conn} do
old = Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, confirmation_pending: true)
conn =
conn
|> assign(:user, user)
|> UserEnabledPlug.call(%{})
assert conn.assigns.user == nil
Pleroma.Config.put([:instance, :account_activation_required], old)
end
test "with a user that is deactivated, it removes that user", %{conn: conn} do test "with a user that is deactivated, it removes that user", %{conn: conn} do
user = insert(:user, deactivated: true) user = insert(:user, deactivated: true)

View file

@ -1195,6 +1195,13 @@ test "auth_active?/1 works correctly" do
refute User.auth_active?(local_user) refute User.auth_active?(local_user)
assert User.auth_active?(confirmed_user) assert User.auth_active?(confirmed_user)
assert User.auth_active?(remote_user) assert User.auth_active?(remote_user)
# also shows unactive for deactivated users
deactivated_but_confirmed =
insert(:user, local: true, confirmation_pending: false, deactivated: true)
refute User.auth_active?(deactivated_but_confirmed)
end end
describe "superuser?/1" do describe "superuser?/1" do

View file

@ -812,6 +812,78 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end end
end end
describe "react to an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Pleroma.Web.Federator.publish(reaction_activity))
end
test "adds an emoji reaction activity to the db" do
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert reaction_activity
assert reaction_activity.data["actor"] == reactor.ap_id
assert reaction_activity.data["type"] == "EmojiReaction"
assert reaction_activity.data["content"] == "🔥"
assert reaction_activity.data["object"] == object.data["id"]
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1
assert object.data["reactions"]["🔥"] == [reactor.ap_id]
end
end
describe "unreacting to an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Pleroma.Web.Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert called(Pleroma.Web.Federator.publish(unreaction_activity))
end
test "adds an undo activity to the db" do
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert unreaction_activity.actor == reactor.ap_id
assert unreaction_activity.data["object"] == reaction_activity.data["id"]
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == %{}
end
end
describe "like an object" do describe "like an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true) Pleroma.Config.put([:instance, :federating], true)

View file

@ -0,0 +1,105 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
use Pleroma.DataCase
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy
alias Pleroma.Web.ActivityPub.Visibility
clear_config([:mrf_object_age]) do
Config.put(:mrf_object_age,
threshold: 172_800,
actions: [:delist, :strip_followers]
)
end
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "with reject action" do
test "it rejects an old post" do
Config.put([:mrf_object_age, :actions], [:reject])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
{:reject, _} = ObjectAgePolicy.filter(data)
end
test "it allows a new post" do
Config.put([:mrf_object_age, :actions], [:reject])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
{:ok, _} = ObjectAgePolicy.filter(data)
end
end
describe "with delist action" do
test "it delists an old post" do
Config.put([:mrf_object_age, :actions], [:delist])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
{:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
{:ok, data} = ObjectAgePolicy.filter(data)
assert Visibility.get_visibility(%{data: data}) == "unlisted"
end
test "it allows a new post" do
Config.put([:mrf_object_age, :actions], [:delist])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
{:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
{:ok, ^data} = ObjectAgePolicy.filter(data)
end
end
describe "with strip_followers action" do
test "it strips followers collections from an old post" do
Config.put([:mrf_object_age, :actions], [:strip_followers])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
{:ok, data} = ObjectAgePolicy.filter(data)
refute user.follower_address in data["to"]
refute user.follower_address in data["cc"]
end
test "it allows a new post" do
Config.put([:mrf_object_age, :actions], [:strip_followers])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
{:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
{:ok, ^data} = ObjectAgePolicy.filter(data)
end
end
end

View file

@ -339,6 +339,80 @@ test "it works for incoming likes" do
assert data["object"] == activity.data["object"] assert data["object"] == activity.data["object"]
end end
test "it works for incoming misskey likes, turning them into EmojiReactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
assert data["type"] == "EmojiReaction"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == "🍮"
end
test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/misskey-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("_misskey_reaction", "")
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == data["actor"]
assert data["type"] == "EmojiReaction"
assert data["id"] == data["id"]
assert data["object"] == activity.data["object"]
assert data["content"] == ""
end
test "it works for incoming emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "EmojiReaction"
assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
assert data["object"] == activity.data["object"]
assert data["content"] == "👌"
end
test "it works for incoming emoji reaction undos" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
{:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", reaction_activity.data["id"])
|> Map.put("actor", user.ap_id)
{:ok, activity} = Transmogrifier.handle_incoming(data)
assert activity.actor == user.ap_id
assert activity.data["id"] == data["id"]
assert activity.data["type"] == "Undo"
end
test "it returns an error for incoming unlikes wihout a like activity" do test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
@ -553,6 +627,20 @@ test "it strips internal likes" do
refute Map.has_key?(object.data, "likes") refute Map.has_key?(object.data, "likes")
end end
test "it strips internal reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
%{object: object} = Activity.get_by_id_with_object(activity.id)
assert Map.has_key?(object.data, "reactions")
assert Map.has_key?(object.data, "reaction_count")
object_data = Transmogrifier.strip_internal_fields(object.data)
refute Map.has_key?(object_data, "reactions")
refute Map.has_key?(object_data, "reaction_count")
end
test "it works for incoming update activities" do test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()

View file

@ -636,4 +636,47 @@ test "removes actor from announcements" do
assert updated_object.data["announcement_count"] == 1 assert updated_object.data["announcement_count"] == 1
end end
end end
describe "get_reports_grouped_by_status/1" do
setup do
[reporter, target_user] = insert_pair(:user)
first_status = insert(:note_activity, user: target_user)
second_status = insert(:note_activity, user: target_user)
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [first_status.id]
})
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended2",
"status_ids" => [second_status.id]
})
data = [%{activity: first_status.data["id"]}, %{activity: second_status.data["id"]}]
{:ok,
%{
first_status: first_status,
second_status: second_status,
data: data
}}
end
test "works for deprecated reports format", %{
first_status: first_status,
second_status: second_status,
data: data
} do
groups = Utils.get_reports_grouped_by_status(data).groups
first_group = Enum.find(groups, &(&1.status.id == first_status.data["id"]))
second_group = Enum.find(groups, &(&1.status.id == second_status.data["id"]))
assert first_group.status.id == first_status.data["id"]
assert second_group.status.id == second_status.data["id"]
end
end
end end

View file

@ -1312,7 +1312,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do
end end
end end
describe "PUT /api/pleroma/admin/reports/:id" do describe "PATCH /api/pleroma/admin/reports" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user) [reporter, target_user] = insert_pair(:user)
@ -1325,16 +1325,32 @@ test "returns 404 when report id is invalid", %{conn: conn} do
"status_ids" => [activity.id] "status_ids" => [activity.id]
}) })
%{conn: assign(conn, :user, admin), id: report_id, admin: admin} {:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended",
"status_ids" => [activity.id]
})
%{
conn: assign(conn, :user, admin),
id: report_id,
admin: admin,
second_report_id: second_report_id
}
end end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
response = conn
conn |> patch("/api/pleroma/admin/reports", %{
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) "reports" => [
|> json_response(:ok) %{"state" => "resolved", "id" => id}
]
})
|> json_response(:no_content)
assert response["state"] == "resolved" activity = Activity.get_by_id(id)
assert activity.data["state"] == "resolved"
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
@ -1343,12 +1359,16 @@ test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
end end
test "closes report", %{conn: conn, id: id, admin: admin} do test "closes report", %{conn: conn, id: id, admin: admin} do
response = conn
conn |> patch("/api/pleroma/admin/reports", %{
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) "reports" => [
|> json_response(:ok) %{"state" => "closed", "id" => id}
]
})
|> json_response(:no_content)
assert response["state"] == "closed" activity = Activity.get_by_id(id)
assert activity.data["state"] == "closed"
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
@ -1359,17 +1379,54 @@ test "closes report", %{conn: conn, id: id, admin: admin} do
test "returns 400 when state is unknown", %{conn: conn, id: id} do test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn = conn =
conn conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"}) |> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})
assert json_response(conn, :bad_request) == "Unsupported state" assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end end
test "returns 404 when report is not exist", %{conn: conn} do test "returns 404 when report is not exist", %{conn: conn} do
conn = conn =
conn conn
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"}) |> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})
assert json_response(conn, :not_found) == "Not found" assert hd(json_response(conn, :bad_request))["error"] == "not_found"
end
test "updates state of multiple reports", %{
conn: conn,
id: id,
admin: admin,
second_report_id: second_report_id
} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
second_activity = Activity.get_by_id(second_report_id)
assert activity.data["state"] == "resolved"
assert second_activity.data["state"] == "closed"
[first_log_entry, second_log_entry] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(first_log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
end end
end end
@ -1492,7 +1549,145 @@ test "returns 403 when requested by anonymous" do
end end
end end
# describe "GET /api/pleroma/admin/grouped_reports" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
first_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date1})
second_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date2})
third_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date3})
{:ok, first_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id, third_status.id]
})
{:ok, second_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id]
})
{:ok, third_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id]
})
%{
conn: assign(conn, :user, admin),
first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
first_status_reports: [first_report, second_report, third_report],
second_status_reports: [first_report, second_report],
third_status_reports: [first_report],
target_user: target_user,
reporter: reporter
}
end
test "returns reports grouped by status", %{
conn: conn,
first_status: first_status,
second_status: second_status,
third_status: third_status,
first_status_reports: first_status_reports,
second_status_reports: second_status_reports,
third_status_reports: third_status_reports,
target_user: target_user,
reporter: reporter
} do
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
assert length(response["reports"]) == 3
first_group =
Enum.find(response["reports"], &(&1["status"]["id"] == first_status.data["id"]))
second_group =
Enum.find(response["reports"], &(&1["status"]["id"] == second_status.data["id"]))
third_group =
Enum.find(response["reports"], &(&1["status"]["id"] == third_status.data["id"]))
assert length(first_group["reports"]) == 3
assert length(second_group["reports"]) == 2
assert length(third_group["reports"]) == 1
assert first_group["date"] ==
Enum.max_by(first_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert first_group["status"] == %{
"id" => first_status.data["id"],
"content" => first_status.object.data["content"],
"published" => first_status.object.data["published"]
}
assert first_group["account"]["id"] == target_user.id
assert length(first_group["actors"]) == 1
assert hd(first_group["actors"])["id"] == reporter.id
assert Enum.map(first_group["reports"], & &1["id"]) --
Enum.map(first_status_reports, & &1.id) == []
assert second_group["date"] ==
Enum.max_by(second_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert second_group["status"] == %{
"id" => second_status.data["id"],
"content" => second_status.object.data["content"],
"published" => second_status.object.data["published"]
}
assert second_group["account"]["id"] == target_user.id
assert length(second_group["actors"]) == 1
assert hd(second_group["actors"])["id"] == reporter.id
assert Enum.map(second_group["reports"], & &1["id"]) --
Enum.map(second_status_reports, & &1.id) == []
assert third_group["date"] ==
Enum.max_by(third_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert third_group["status"] == %{
"id" => third_status.data["id"],
"content" => third_status.object.data["content"],
"published" => third_status.object.data["published"]
}
assert third_group["account"]["id"] == target_user.id
assert length(third_group["actors"]) == 1
assert hd(third_group["actors"])["id"] == reporter.id
assert Enum.map(third_group["reports"], & &1["id"]) --
Enum.map(third_status_reports, & &1.id) == []
end
end
describe "POST /api/pleroma/admin/reports/:id/respond" do describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)

View file

@ -227,6 +227,33 @@ test "it can handle activities that expire" do
end end
describe "reactions" do describe "reactions" do
test "reacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
# TODO: test error case.
end
test "unreacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
{:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"]
end
test "repeating a status" do test "repeating a status" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -441,6 +468,35 @@ test "does not update report state when state is unsupported" do
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"} assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end end
test "updates state of multiple reports" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended!",
"status_ids" => [activity.id]
})
{:ok, report_ids} =
CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
first_report = Activity.get_by_id(first_report_id)
second_report = Activity.get_by_id(second_report_id)
assert report_ids -- [first_report_id, second_report_id] == []
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
end end
describe "reblog muting" do describe "reblog muting" do

View file

@ -61,6 +61,33 @@ test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do
assert Pleroma.Application.repository() == result["software"]["repository"] assert Pleroma.Application.repository() == result["software"]["repository"]
end end
test "returns fieldsLimits field", %{conn: conn} do
max_account_fields = Pleroma.Config.get([:instance, :max_account_fields])
max_remote_account_fields = Pleroma.Config.get([:instance, :max_remote_account_fields])
account_field_name_length = Pleroma.Config.get([:instance, :account_field_name_length])
account_field_value_length = Pleroma.Config.get([:instance, :account_field_value_length])
Pleroma.Config.put([:instance, :max_account_fields], 10)
Pleroma.Config.put([:instance, :max_remote_account_fields], 15)
Pleroma.Config.put([:instance, :account_field_name_length], 255)
Pleroma.Config.put([:instance, :account_field_value_length], 2048)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert response["metadata"]["fieldsLimits"]["maxFields"] == 10
assert response["metadata"]["fieldsLimits"]["maxRemoteFields"] == 15
assert response["metadata"]["fieldsLimits"]["nameLength"] == 255
assert response["metadata"]["fieldsLimits"]["valueLength"] == 2048
Pleroma.Config.put([:instance, :max_account_fields], max_account_fields)
Pleroma.Config.put([:instance, :max_remote_account_fields], max_remote_account_fields)
Pleroma.Config.put([:instance, :account_field_name_length], account_field_name_length)
Pleroma.Config.put([:instance, :account_field_value_length], account_field_value_length)
end
test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
option = Pleroma.Config.get([:instance, :safe_dm_mentions]) option = Pleroma.Config.get([:instance, :safe_dm_mentions])
Pleroma.Config.put([:instance, :safe_dm_mentions], true) Pleroma.Config.put([:instance, :safe_dm_mentions], true)

View file

@ -7,12 +7,72 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
test "POST /api/v1/pleroma/statuses/:id/react_with_emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
result =
conn
|> assign(:user, other_user)
|> post("/api/v1/pleroma/statuses/#{activity.id}/react_with_emoji", %{"emoji" => ""})
assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id
end
test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "")
result =
conn
|> assign(:user, other_user)
|> post("/api/v1/pleroma/statuses/#{activity.id}/unreact_with_emoji", %{"emoji" => ""})
assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id
object = Object.normalize(activity)
assert object.data["reaction_count"] == 0
end
test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
|> json_response(200)
assert result == %{}
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
|> json_response(200)
[represented_user] = result["🎅"]
assert represented_user["id"] == other_user.id
end
test "/api/v1/pleroma/conversations/:id", %{conn: conn} do test "/api/v1/pleroma/conversations/:id", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -1,5 +1,6 @@
defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -128,6 +129,34 @@ test "shows the whole thread", %{conn: conn} do
assert html =~ "voyages" assert html =~ "voyages"
end end
test "redirect by AP object ID", %{conn: conn} do
user = insert(:user)
{:ok, %Activity{data: %{"object" => object_url}}} =
CommonAPI.post(user, %{"status" => "beam me up"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get(URI.parse(object_url).path)
assert html_response(conn, 302) =~ "redirected"
end
test "redirect by activity ID", %{conn: conn} do
user = insert(:user)
{:ok, %Activity{data: %{"id" => id}}} =
CommonAPI.post(user, %{"status" => "I'm a doctor, not a devops!"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get(URI.parse(id).path)
assert html_response(conn, 302) =~ "redirected"
end
test "404 when notice not found", %{conn: conn} do test "404 when notice not found", %{conn: conn} do
conn = conn =
conn conn
@ -151,7 +180,7 @@ test "404 for private status", %{conn: conn} do
assert html_response(conn, 404) =~ "not found" assert html_response(conn, 404) =~ "not found"
end end
test "404 for remote cached status", %{conn: conn} do test "302 for remote cached status", %{conn: conn} do
user = insert(:user) user = insert(:user)
message = %{ message = %{
@ -175,7 +204,7 @@ test "404 for remote cached status", %{conn: conn} do
|> put_req_header("accept", "text/html") |> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}") |> get("/notice/#{activity.id}")
assert html_response(conn, 404) =~ "not found" assert html_response(conn, 302) =~ "redirected"
end end
end end
end end