Merge branch 'develop' into feature/admin-api-list-statuses-for-a-given-instance
This commit is contained in:
commit
114930b5fd
@ -34,7 +34,7 @@ benchmark:
|
||||
variables:
|
||||
MIX_ENV: benchmark
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: postgres:9.6
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
@ -46,19 +46,19 @@ benchmark:
|
||||
unit-testing:
|
||||
stage: test
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: postgres:9.6
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mix coveralls --trace --preload-modules
|
||||
- mix coveralls --preload-modules
|
||||
|
||||
unit-testing-rum:
|
||||
stage: test
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: minibikini/postgres-with-rum:12
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
variables:
|
||||
@ -68,7 +68,7 @@ unit-testing-rum:
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
|
||||
- mix test --trace --preload-modules
|
||||
- mix test --preload-modules
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
@ -113,6 +113,7 @@ review_app:
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
|
||||
- ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
|
||||
- (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true
|
||||
- (ssh -t dokku@pleroma.online -- git:set "$CI_ENVIRONMENT_SLUG" keep-git-dir true) || true
|
||||
- ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku
|
||||
- (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true
|
||||
- (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
|
||||
@ -138,7 +139,7 @@ stop_review_app:
|
||||
- ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db
|
||||
|
||||
amd64:
|
||||
stage: release
|
||||
stage: release
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0
|
||||
only: &release-only
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -25,6 +25,7 @@ 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: 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.
|
||||
- Admin API: Return `total` when querying for reports
|
||||
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
|
||||
@ -38,12 +39,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
### Added
|
||||
- Refreshing poll results for remote polls
|
||||
- Authentication: Added rate limit for password-authorized actions / login existence checks
|
||||
- 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`)
|
||||
- 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).
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- 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
|
||||
- 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
|
||||
@ -52,19 +55,24 @@ 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
|
||||
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
|
||||
- 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
|
||||
- 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
|
||||
- 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 the `recipients` parameter to `GET /api/v1/conversations`
|
||||
- Configuration: `feed` option for user atom feed.
|
||||
- Pleroma API: Add Emoji reactions
|
||||
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
|
||||
</details>
|
||||
|
||||
### Fixed
|
||||
- Report emails now include functional links to profiles of remote user accounts
|
||||
- Not being able to log in to some third-party apps when logged in to MastoFE
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
|
@ -274,7 +274,7 @@
|
||||
account_field_name_length: 512,
|
||||
account_field_value_length: 2048,
|
||||
external_user_synchronization: true,
|
||||
extended_nickname_format: false
|
||||
extended_nickname_format: true
|
||||
|
||||
config :pleroma, :feed,
|
||||
post_title: %{
|
||||
@ -605,6 +605,8 @@
|
||||
|
||||
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
||||
|
||||
config :pleroma, :static_fe, enabled: false
|
||||
|
||||
config :pleroma, :web_cache_ttl,
|
||||
activity_pub: nil,
|
||||
activity_pub_question: 30_000
|
||||
|
@ -2,11 +2,10 @@
|
||||
|
||||
Authentication is required and the user must be an admin.
|
||||
|
||||
## `/api/pleroma/admin/users`
|
||||
## `GET /api/pleroma/admin/users`
|
||||
|
||||
### List users
|
||||
|
||||
- Method `GET`
|
||||
- Query Params:
|
||||
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
|
||||
- *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
|
||||
|
||||
- Method `DELETE`
|
||||
- Params:
|
||||
- `nickname`
|
||||
- Response: User’s nickname
|
||||
@ -60,7 +58,6 @@ Authentication is required and the user must be an admin.
|
||||
|
||||
### Remove a user
|
||||
|
||||
- Method `DELETE`
|
||||
- Params:
|
||||
- `nicknames`
|
||||
- Response: Array of user nicknames
|
||||
@ -78,31 +75,30 @@ Authentication is required and the user must be an admin.
|
||||
]
|
||||
- Response: User’s nickname
|
||||
|
||||
## `/api/pleroma/admin/users/follow`
|
||||
## `POST /api/pleroma/admin/users/follow`
|
||||
|
||||
### Make a user follow another user
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `follower`: The nickname of the follower
|
||||
- `followed`: The nickname of the followed
|
||||
- `follower`: The nickname of the follower
|
||||
- `followed`: The nickname of the followed
|
||||
- Response:
|
||||
- "ok"
|
||||
- "ok"
|
||||
|
||||
## `POST /api/pleroma/admin/users/unfollow`
|
||||
|
||||
## `/api/pleroma/admin/users/unfollow`
|
||||
### Make a user unfollow another user
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `follower`: The nickname of the follower
|
||||
- `followed`: The nickname of the followed
|
||||
- `follower`: The nickname of the follower
|
||||
- `followed`: The nickname of the followed
|
||||
- Response:
|
||||
- "ok"
|
||||
- "ok"
|
||||
|
||||
## `/api/pleroma/admin/users/:nickname/toggle_activation`
|
||||
## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
|
||||
|
||||
### Toggle user activation
|
||||
|
||||
- Method: `PATCH`
|
||||
- Params:
|
||||
- `nickname`
|
||||
- Response: User’s 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
|
||||
|
||||
- Method: `PUT`
|
||||
- Params:
|
||||
- `nicknames` (array)
|
||||
- `tags` (array)
|
||||
|
||||
## `DELETE /api/pleroma/admin/users/tag`
|
||||
|
||||
### Untag a list of users
|
||||
|
||||
- Method: `DELETE`
|
||||
- Params:
|
||||
- `nicknames` (array)
|
||||
- `tags` (array)
|
||||
|
||||
## `/api/pleroma/admin/users/:nickname/permission_group`
|
||||
## `GET /api/pleroma/admin/users/:nickname/permission_group`
|
||||
|
||||
### Get user user permission groups membership
|
||||
|
||||
- Method: `GET`
|
||||
- Params: none
|
||||
- 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 doesn’t exist.
|
||||
|
||||
### Get user user permission groups membership per permission group
|
||||
|
||||
- Method: `GET`
|
||||
- Params: none
|
||||
- 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`
|
||||
|
||||
## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
|
||||
|
||||
### Remove user from permission group
|
||||
|
||||
- Params: none
|
||||
@ -247,22 +243,20 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- `nickname`
|
||||
- `status` BOOLEAN field, false value means deactivation.
|
||||
|
||||
## `/api/pleroma/admin/users/:nickname_or_id`
|
||||
## `GET /api/pleroma/admin/users/:nickname_or_id`
|
||||
|
||||
### Retrive the details of a user
|
||||
|
||||
- Method: `GET`
|
||||
- Params:
|
||||
- `nickname` or `id`
|
||||
- Response:
|
||||
- On failure: `Not found`
|
||||
- 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
|
||||
|
||||
- Method: `GET`
|
||||
- Params:
|
||||
- `nickname` or `id`
|
||||
- *optional* `page_size`: number of statuses to return (default is `20`)
|
||||
@ -271,19 +265,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- On failure: `Not found`
|
||||
- On success: JSON array of user's latest statuses
|
||||
|
||||
## `/api/pleroma/admin/relay`
|
||||
## `POST /api/pleroma/admin/relay`
|
||||
|
||||
### Follow a Relay
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `relay_url`
|
||||
- Response:
|
||||
- On success: URL of the followed relay
|
||||
|
||||
## `DELETE /api/pleroma/admin/relay`
|
||||
|
||||
### Unfollow a Relay
|
||||
|
||||
- Methods: `DELETE`
|
||||
- Params:
|
||||
- `relay_url`
|
||||
- Response:
|
||||
@ -297,11 +291,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- Response:
|
||||
- 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
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- *optional* `max_use` (integer)
|
||||
- *optional* `expires_at` (date string e.g. "2019-04-07")
|
||||
@ -319,11 +312,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
|
||||
|
||||
- Methods: `GET`
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
@ -345,11 +337,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
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `token`
|
||||
- Response:
|
||||
@ -367,21 +358,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## `/api/pleroma/admin/users/email_invite`
|
||||
## `POST /api/pleroma/admin/users/email_invite`
|
||||
|
||||
### Sends registration invite via email
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `email`
|
||||
- `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
|
||||
|
||||
- Methods: `GET`
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
@ -392,18 +380,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
|
||||
|
||||
- Methods: `PATCH`
|
||||
- Params:
|
||||
- `nicknames`
|
||||
- Response: none (code `204`)
|
||||
|
||||
## `/api/pleroma/admin/reports`
|
||||
## `GET /api/pleroma/admin/reports`
|
||||
|
||||
### Get a list of reports
|
||||
- Method `GET`
|
||||
|
||||
- Params:
|
||||
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
|
||||
- *optional* `limit`: **integer** the number of records to retrieve
|
||||
@ -418,7 +406,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
|
||||
```json
|
||||
{
|
||||
"total" : 1,
|
||||
"totalReports" : 1,
|
||||
"reports": [
|
||||
{
|
||||
"account": {
|
||||
@ -560,9 +548,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
|
||||
- Method `GET`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- Response:
|
||||
@ -571,22 +584,41 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- 404 Not Found `"Not found"`
|
||||
- On success: JSON, Report object (see above)
|
||||
|
||||
## `/api/pleroma/admin/reports/:id`
|
||||
### Change the state of the report
|
||||
- Method `PUT`
|
||||
## `PATCH /api/pleroma/admin/reports`
|
||||
|
||||
### Change the state of one or multiple reports
|
||||
|
||||
- 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:
|
||||
- On failure:
|
||||
- 400 Bad Request `"Unsupported state"`
|
||||
- 403 Forbidden `{"error": "error_msg"}`
|
||||
- 404 Not Found `"Not found"`
|
||||
- On success: JSON, Report object (see above)
|
||||
- 400 Bad Request, JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
`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
|
||||
- Method `POST`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- `status`: required, the message
|
||||
@ -656,9 +688,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
|
||||
- Method `PUT`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- `sensitive`: optional, valid values are `true` or `false`
|
||||
@ -670,9 +703,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- 404 Not Found `"Not found"`
|
||||
- On success: JSON, Mastodon Status entity
|
||||
|
||||
## `/api/pleroma/admin/statuses/:id`
|
||||
## `DELETE /api/pleroma/admin/statuses/:id`
|
||||
|
||||
### Delete an individual reported status
|
||||
- Method `DELETE`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- Response:
|
||||
@ -681,11 +715,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||
- 404 Not Found `"Not found"`
|
||||
- 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
|
||||
|
||||
Copy settings on key `:pleroma` to DB.
|
||||
- Method `GET`
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
@ -693,10 +728,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
|
||||
|
||||
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
|
||||
- Method `GET`
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
@ -704,10 +741,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 only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
|
||||
- Method `GET`
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
@ -723,8 +762,10 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
|
||||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/config`
|
||||
## `POST /api/pleroma/admin/config`
|
||||
|
||||
### Update config settings
|
||||
|
||||
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"`.
|
||||
Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
|
||||
@ -747,7 +788,6 @@ Compile time settings (need instance reboot):
|
||||
- `Pleroma.Upload` -> `:proxy_remote`
|
||||
- `:instance` -> `:upload_limit`
|
||||
|
||||
- Method `POST`
|
||||
- Params:
|
||||
- `configs` => [
|
||||
- `group` (string)
|
||||
@ -802,9 +842,10 @@ Compile time settings (need instance reboot):
|
||||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/moderation_log`
|
||||
## `GET /api/pleroma/admin/moderation_log`
|
||||
|
||||
### Get moderation log
|
||||
- Method `GET`
|
||||
|
||||
- Params:
|
||||
- *optional* `page`: **integer** page number
|
||||
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
|
||||
@ -831,8 +872,9 @@ Compile time settings (need instance reboot):
|
||||
```
|
||||
|
||||
## `POST /api/pleroma/admin/reload_emoji`
|
||||
|
||||
### Reload the instance's custom emoji
|
||||
* Method `POST`
|
||||
* Authentication: required
|
||||
* Params: None
|
||||
* Response: JSON, "ok" and 200 status
|
||||
|
||||
- Authentication: required
|
||||
- Params: None
|
||||
- Response: JSON, "ok" and 200 status
|
||||
|
@ -479,3 +479,35 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
||||
* `artist`: the artist of the media playing [optional]
|
||||
* `length`: the length of the media playing [optional]
|
||||
* 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..."}]
|
||||
}
|
||||
```
|
||||
|
@ -180,6 +180,14 @@ config :pleroma, :frontend_configurations,
|
||||
|
||||
These settings **need to be complete**, they will override the defaults.
|
||||
|
||||
### :static_fe
|
||||
|
||||
Render profiles and posts using server-generated HTML that is viewable without using JavaScript.
|
||||
|
||||
Available options:
|
||||
|
||||
* `enabled` - Enables the rendering of static HTML. Defaults to `false`.
|
||||
|
||||
### :assets
|
||||
|
||||
This section configures assets to be used with various frontends. Currently the only option
|
||||
@ -523,7 +531,7 @@ config :pleroma, :workers,
|
||||
|
||||
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
|
||||
|
||||
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
|
||||
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
|
||||
|
||||
Example:
|
||||
|
||||
@ -593,6 +601,10 @@ See the [Quack Github](https://github.com/azohra/quack) for more details
|
||||
## Database options
|
||||
|
||||
### RUM indexing for full text search
|
||||
|
||||
!!! warning
|
||||
It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
|
||||
|
||||
* `rum_enabled`: If RUM indexes should be used. Defaults to `false`.
|
||||
|
||||
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. While they may eventually be mainlined, for now they have to be installed as a PostgreSQL extension from https://github.com/postgrespro/rum.
|
||||
@ -793,4 +805,3 @@ config :auto_linker,
|
||||
rel: "ugc"
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -1,9 +1,13 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
The following packages need to be installed:
|
||||
|
||||
* elixir
|
||||
* gmake
|
||||
* ImageMagick
|
||||
@ -11,8 +15,11 @@ The following packages need to be installed:
|
||||
* postgresql-server
|
||||
* postgresql-contrib
|
||||
|
||||
To install them, run the following command (with doas or as root):
|
||||
`pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib`
|
||||
To install them, run the following command (with doas or as root):
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
@ -31,8 +38,8 @@ Create the \_pleroma user, assign it the pleroma login class and create its home
|
||||
#### 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.
|
||||
|
||||
#### Postgresql
|
||||
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
|
||||
#### 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.
|
||||
|
||||
When this is done, enable postgresql so that it starts on boot and start it. As root, run:
|
||||
@ -44,6 +51,7 @@ To check that it started properly and didn't fail right after starting, you can
|
||||
|
||||
#### httpd
|
||||
httpd will have three fuctions:
|
||||
|
||||
* redirect requests trying to reach the instance over http to the https URL
|
||||
* serve a robots.txt file
|
||||
* get Let's Encrypt certificates, with acme-client
|
||||
@ -76,9 +84,9 @@ 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):
|
||||
```
|
||||
rcctl enable httpd
|
||||
@ -86,7 +94,7 @@ rcctl start httpd
|
||||
```
|
||||
|
||||
#### acme-client
|
||||
acme-client is used to get SSL/TLS certificates from Let's Encrypt.
|
||||
acme-client is used to get SSL/TLS certificates from Let's Encrypt.
|
||||
Insert the following configuration in /etc/acme-client.conf:
|
||||
```
|
||||
#
|
||||
@ -107,7 +115,7 @@ domain <domain name> {
|
||||
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`.
|
||||
|
||||
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:
|
||||
@ -118,7 +126,7 @@ ln -s /etc/ssl/private/<domain name>.key /etc/ssl/private/<IP address>.key
|
||||
This will have to be done for each IPv4 and IPv6 address relayd listens on.
|
||||
|
||||
#### relayd
|
||||
relayd will be used as the reverse proxy sitting in front of pleroma.
|
||||
relayd will be used as the reverse proxy sitting in front of pleroma.
|
||||
Insert the following configuration in /etc/relayd.conf:
|
||||
```
|
||||
# $OpenBSD: relayd.conf,v 1.4 2018/03/23 09:55:06 claudio Exp $
|
||||
@ -169,7 +177,7 @@ relay wwwtls {
|
||||
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):
|
||||
```
|
||||
rcctl enable relayd
|
||||
@ -177,7 +185,7 @@ rcctl start relayd
|
||||
```
|
||||
|
||||
#### pf
|
||||
Enabling and configuring pf is highly recommended.
|
||||
Enabling and configuring pf is highly recommended.
|
||||
In /etc/pf.conf, insert the following configuration:
|
||||
```
|
||||
# Macros
|
||||
@ -202,20 +210,22 @@ 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 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`.
|
||||
|
||||
#### 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:
|
||||
|
||||
* run `mix deps.get`
|
||||
* 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.
|
||||
* exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/config/setup_db.psql` to setup the database.
|
||||
* return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate`
|
||||
|
||||
As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance.
|
||||
As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance.
|
||||
In another SSH session/tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. Double-check that *uri*'s value is your instance's domain name.
|
||||
|
||||
##### Starting pleroma at boot
|
||||
|
@ -42,6 +42,10 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
|
||||
## Setup
|
||||
### Configuring PostgreSQL
|
||||
#### (Optional) Installing RUM indexes
|
||||
|
||||
!!! warning
|
||||
It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
|
||||
|
||||
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results).
|
||||
|
||||
Debian/Ubuntu (available only on Buster/19.04):
|
||||
@ -74,7 +78,7 @@ rc-service postgresql restart
|
||||
# Create the Pleroma user
|
||||
adduser --system --shell /bin/false --home /opt/pleroma pleroma
|
||||
|
||||
# Set the flavour environment variable to the string you got in Detecting flavour section.
|
||||
# Set the flavour environment variable to the string you got in Detecting flavour section.
|
||||
# For example if the flavour is `arm64-musl` the command will be
|
||||
export FLAVOUR="arm64-musl"
|
||||
|
||||
@ -180,7 +184,7 @@ rc-service pleroma start
|
||||
rc-update add pleroma
|
||||
```
|
||||
|
||||
If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
|
||||
If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
|
||||
|
||||
Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://webchat.freenode.net/?channels=%23pleroma) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new)
|
||||
|
||||
|
@ -45,7 +45,7 @@ def run(["migrate_from_db", env, delete?]) do
|
||||
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
|
||||
config_path = "config/#{env}.exported_from_db.secret.exs"
|
||||
|
||||
{:ok, file} = File.open(config_path, [:write])
|
||||
{:ok, file} = File.open(config_path, [:write, :utf8])
|
||||
IO.write(file, "use Mix.Config\r\n")
|
||||
|
||||
Repo.all(Config)
|
||||
|
@ -41,6 +41,10 @@ defmodule Pleroma.Activity do
|
||||
field(:actor, :string)
|
||||
field(:recipients, {:array, :string}, default: [])
|
||||
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
|
||||
has_one(:bookmark, Bookmark)
|
||||
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)
|
||||
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
|
||||
from([a] in query,
|
||||
left_join: b in Bookmark,
|
||||
|
@ -36,7 +36,8 @@ def start(_type, _args) do
|
||||
Pleroma.Emoji,
|
||||
Pleroma.Captcha,
|
||||
Pleroma.Daemons.ScheduledActivityDaemon,
|
||||
Pleroma.Daemons.ActivityExpirationDaemon
|
||||
Pleroma.Daemons.ActivityExpirationDaemon,
|
||||
Pleroma.Plugs.RateLimiter.Supervisor
|
||||
] ++
|
||||
cachex_children() ++
|
||||
hackney_pool_children() ++
|
||||
|
@ -9,6 +9,8 @@ defmodule Pleroma.Constants do
|
||||
|
||||
const(object_internal_fields,
|
||||
do: [
|
||||
"reactions",
|
||||
"reaction_count",
|
||||
"likes",
|
||||
"like_count",
|
||||
"announcements",
|
||||
|
@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do
|
||||
def process(descriptions) do
|
||||
config_path = "docs/generate_config.json"
|
||||
|
||||
with {:ok, file} <- File.open(config_path, [:write]),
|
||||
with {:ok, file} <- File.open(config_path, [:write, :utf8]),
|
||||
json <- generate_json(descriptions),
|
||||
:ok <- IO.write(file, json),
|
||||
:ok <- File.close(file) do
|
||||
|
769
lib/pleroma/emoji-data.txt
Normal file
769
lib/pleroma/emoji-data.txt
Normal 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 worker’s 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 o’clock..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 o’clock..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 o’clock..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
|
@ -98,4 +98,35 @@ def code_change(_old_vsn, state, _extra) do
|
||||
defp update_emojis(emojis) do
|
||||
:ets.insert(@ets, emojis)
|
||||
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
|
||||
|
@ -64,6 +64,8 @@ def contain_origin(id, %{"actor" => _actor} = params) do
|
||||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||
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
|
||||
id_uri = URI.parse(id)
|
||||
other_uri = URI.parse(other_id)
|
||||
|
@ -1,131 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.RateLimiter do
|
||||
@moduledoc """
|
||||
|
||||
## Configuration
|
||||
|
||||
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
|
||||
|
||||
* The first element: `scale` (Integer). The time scale in milliseconds.
|
||||
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
|
||||
|
||||
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
|
||||
|
||||
To disable a limiter set its value to `nil`.
|
||||
|
||||
### Example
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
one: {1000, 10},
|
||||
two: [{10_000, 10}, {10_000, 50}],
|
||||
foobar: nil
|
||||
|
||||
Here we have three limiters:
|
||||
|
||||
* `one` which is not over 10req/1s
|
||||
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
|
||||
* `foobar` which is disabled
|
||||
|
||||
## Usage
|
||||
|
||||
AllowedSyntax:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :limiter_name)
|
||||
plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
|
||||
|
||||
Allowed options:
|
||||
|
||||
* `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
|
||||
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
|
||||
|
||||
Inside a controller:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
|
||||
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
|
||||
when action in ~w(fav_status unfav_status)a
|
||||
)
|
||||
|
||||
or inside a router pipeline:
|
||||
|
||||
pipeline :api do
|
||||
...
|
||||
plug(Pleroma.Plugs.RateLimiter, :one)
|
||||
...
|
||||
end
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
def init(limiter_name) when is_atom(limiter_name) do
|
||||
init({limiter_name, []})
|
||||
end
|
||||
|
||||
def init({limiter_name, opts}) do
|
||||
case Pleroma.Config.get([:rate_limit, limiter_name]) do
|
||||
nil -> nil
|
||||
config -> {limiter_name, config, opts}
|
||||
end
|
||||
end
|
||||
|
||||
# Do not limit if there is no limiter configuration
|
||||
def call(conn, nil), do: conn
|
||||
|
||||
def call(conn, settings) do
|
||||
case check_rate(conn, settings) do
|
||||
{:ok, _count} ->
|
||||
conn
|
||||
|
||||
{:error, _count} ->
|
||||
render_throttled_error(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp bucket_name(conn, limiter_name, opts) do
|
||||
bucket_name = opts[:bucket_name] || limiter_name
|
||||
|
||||
if params_names = opts[:params] do
|
||||
params_values = for p <- Enum.sort(params_names), do: conn.params[p]
|
||||
Enum.join([bucket_name] ++ params_values, ":")
|
||||
else
|
||||
bucket_name
|
||||
end
|
||||
end
|
||||
|
||||
defp check_rate(
|
||||
%{assigns: %{user: %User{id: user_id}}} = conn,
|
||||
{limiter_name, [_, {scale, limit}], opts}
|
||||
) do
|
||||
bucket_name = bucket_name(conn, limiter_name, opts)
|
||||
ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
|
||||
end
|
||||
|
||||
defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
|
||||
bucket_name = bucket_name(conn, limiter_name, opts)
|
||||
ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
|
||||
end
|
||||
|
||||
defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
|
||||
check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
|
||||
end
|
||||
|
||||
def ip(%{remote_ip: remote_ip}) do
|
||||
remote_ip
|
||||
|> Tuple.to_list()
|
||||
|> Enum.join(".")
|
||||
end
|
||||
|
||||
defp render_throttled_error(conn) do
|
||||
conn
|
||||
|> render_error(:too_many_requests, "Throttled")
|
||||
|> halt()
|
||||
end
|
||||
end
|
44
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
Normal file
44
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
Normal file
@ -0,0 +1,44 @@
|
||||
defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
import Cachex.Spec
|
||||
|
||||
def start_link(init_arg) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def add_limiter(limiter_name, expiration) do
|
||||
{:ok, _pid} =
|
||||
DynamicSupervisor.start_child(
|
||||
__MODULE__,
|
||||
%{
|
||||
id: String.to_atom("rl_#{limiter_name}"),
|
||||
start:
|
||||
{Cachex, :start_link,
|
||||
[
|
||||
limiter_name,
|
||||
[
|
||||
expiration:
|
||||
expiration(
|
||||
default: expiration,
|
||||
interval: check_interval(expiration),
|
||||
lazy: true
|
||||
)
|
||||
]
|
||||
]}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init_arg) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
|
||||
defp check_interval(exp) do
|
||||
(exp / 2)
|
||||
|> Kernel.trunc()
|
||||
|> Kernel.min(5000)
|
||||
|> Kernel.max(1)
|
||||
end
|
||||
end
|
227
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
Normal file
227
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
Normal file
@ -0,0 +1,227 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.RateLimiter do
|
||||
@moduledoc """
|
||||
|
||||
## Configuration
|
||||
|
||||
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
|
||||
|
||||
* The first element: `scale` (Integer). The time scale in milliseconds.
|
||||
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
|
||||
|
||||
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
|
||||
|
||||
To disable a limiter set its value to `nil`.
|
||||
|
||||
### Example
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
one: {1000, 10},
|
||||
two: [{10_000, 10}, {10_000, 50}],
|
||||
foobar: nil
|
||||
|
||||
Here we have three limiters:
|
||||
|
||||
* `one` which is not over 10req/1s
|
||||
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
|
||||
* `foobar` which is disabled
|
||||
|
||||
## Usage
|
||||
|
||||
AllowedSyntax:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
|
||||
plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
|
||||
|
||||
Allowed options:
|
||||
|
||||
* `name` required, always used to fetch the limit values from the config
|
||||
* `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
|
||||
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
|
||||
|
||||
Inside a controller:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
|
||||
when action in ~w(fav_status unfav_status)a
|
||||
)
|
||||
|
||||
or inside a router pipeline:
|
||||
|
||||
pipeline :api do
|
||||
...
|
||||
plug(Pleroma.Plugs.RateLimiter, name: :one)
|
||||
...
|
||||
end
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
|
||||
alias Pleroma.User
|
||||
|
||||
def init(opts) do
|
||||
limiter_name = Keyword.get(opts, :name)
|
||||
|
||||
case Pleroma.Config.get([:rate_limit, limiter_name]) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
config ->
|
||||
name_root = Keyword.get(opts, :bucket_name, limiter_name)
|
||||
|
||||
%{
|
||||
name: name_root,
|
||||
limits: config,
|
||||
opts: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Do not limit if there is no limiter configuration
|
||||
def call(conn, nil), do: conn
|
||||
|
||||
def call(conn, settings) do
|
||||
settings
|
||||
|> incorporate_conn_info(conn)
|
||||
|> check_rate()
|
||||
|> case do
|
||||
{:ok, _count} ->
|
||||
conn
|
||||
|
||||
{:error, _count} ->
|
||||
render_throttled_error(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_bucket(conn, name_root, settings) do
|
||||
settings =
|
||||
settings
|
||||
|> incorporate_conn_info(conn)
|
||||
|
||||
bucket_name = make_bucket_name(%{settings | name: name_root})
|
||||
key_name = make_key_name(settings)
|
||||
limit = get_limits(settings)
|
||||
|
||||
case Cachex.get(bucket_name, key_name) do
|
||||
{:error, :no_cache} ->
|
||||
{:err, :not_found}
|
||||
|
||||
{:ok, nil} ->
|
||||
{0, limit}
|
||||
|
||||
{:ok, value} ->
|
||||
{value, limit - value}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_rate(settings) do
|
||||
bucket_name = make_bucket_name(settings)
|
||||
key_name = make_key_name(settings)
|
||||
limit = get_limits(settings)
|
||||
|
||||
case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
|
||||
{:commit, value} ->
|
||||
{:ok, value}
|
||||
|
||||
{:ignore, value} ->
|
||||
{:error, value}
|
||||
|
||||
{:error, :no_cache} ->
|
||||
initialize_buckets(settings)
|
||||
check_rate(settings)
|
||||
end
|
||||
end
|
||||
|
||||
defp increment_value(nil, _limit), do: {:commit, 1}
|
||||
|
||||
defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
|
||||
|
||||
defp increment_value(val, _limit), do: {:commit, val + 1}
|
||||
|
||||
defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
|
||||
Map.merge(settings, %{
|
||||
mode: :user,
|
||||
conn_params: params,
|
||||
conn_info: "#{user_id}"
|
||||
})
|
||||
end
|
||||
|
||||
defp incorporate_conn_info(settings, %{params: params} = conn) do
|
||||
Map.merge(settings, %{
|
||||
mode: :anon,
|
||||
conn_params: params,
|
||||
conn_info: "#{ip(conn)}"
|
||||
})
|
||||
end
|
||||
|
||||
defp ip(%{remote_ip: remote_ip}) do
|
||||
remote_ip
|
||||
|> Tuple.to_list()
|
||||
|> Enum.join(".")
|
||||
end
|
||||
|
||||
defp render_throttled_error(conn) do
|
||||
conn
|
||||
|> render_error(:too_many_requests, "Throttled")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp make_key_name(settings) do
|
||||
""
|
||||
|> attach_params(settings)
|
||||
|> attach_identity(settings)
|
||||
end
|
||||
|
||||
defp get_scale(_, {scale, _}), do: scale
|
||||
|
||||
defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
|
||||
|
||||
defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
|
||||
|
||||
defp get_limits(%{limits: {_scale, limit}}), do: limit
|
||||
|
||||
defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
|
||||
|
||||
defp get_limits(%{limits: [{_, limit}, _]}), do: limit
|
||||
|
||||
defp make_bucket_name(%{mode: :user, name: name_root}),
|
||||
do: user_bucket_name(name_root)
|
||||
|
||||
defp make_bucket_name(%{mode: :anon, name: name_root}),
|
||||
do: anon_bucket_name(name_root)
|
||||
|
||||
defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
|
||||
param_string =
|
||||
opts
|
||||
|> Keyword.get(:params, [])
|
||||
|> Enum.sort()
|
||||
|> Enum.map(&Map.get(conn_params, &1, ""))
|
||||
|> Enum.join(":")
|
||||
|
||||
"#{input}#{param_string}"
|
||||
end
|
||||
|
||||
defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
|
||||
|
||||
defp initialize_buckets(%{name: name, limits: limits}) do
|
||||
LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
|
||||
LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
|
||||
end
|
||||
|
||||
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
|
||||
do: "user:#{base}:#{conn_info}"
|
||||
|
||||
defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
|
||||
do: "ip:#{base}:#{conn_info}"
|
||||
|
||||
defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
|
||||
defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
|
||||
end
|
16
lib/pleroma/plugs/rate_limiter/supervisor.ex
Normal file
16
lib/pleroma/plugs/rate_limiter/supervisor.ex
Normal file
@ -0,0 +1,16 @@
|
||||
defmodule Pleroma.Plugs.RateLimiter.Supervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_args) do
|
||||
children = [
|
||||
Pleroma.Plugs.RateLimiter.LimiterSupervisor
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
|
||||
Supervisor.init(children, opts)
|
||||
end
|
||||
end
|
26
lib/pleroma/plugs/static_fe_plug.ex
Normal file
26
lib/pleroma/plugs/static_fe_plug.ex
Normal file
@ -0,0 +1,26 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.StaticFEPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.Web.StaticFE.StaticFEController
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(conn, _) do
|
||||
if enabled?() and accepts_html?(conn) do
|
||||
conn
|
||||
|> StaticFEController.call(:show)
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
|
||||
|
||||
defp accepts_html?(conn) do
|
||||
conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html")
|
||||
end
|
||||
end
|
@ -322,6 +322,32 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
||||
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.
|
||||
def like(
|
||||
%User{ap_id: ap_id} = user,
|
||||
|
@ -566,6 +566,34 @@ def handle_incoming(
|
||||
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(
|
||||
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
@ -580,6 +608,27 @@ def handle_incoming(
|
||||
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(
|
||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
@ -715,6 +764,28 @@ def handle_incoming(
|
||||
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(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
@ -1048,7 +1119,7 @@ def prepare_attachments(object) do
|
||||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
||||
defp strip_internal_fields(object) do
|
||||
def strip_internal_fields(object) do
|
||||
object
|
||||
|> Map.drop(Pleroma.Constants.object_internal_fields())
|
||||
end
|
||||
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.Endpoint
|
||||
@ -255,6 +256,16 @@ def get_existing_like(actor, %{data: %{"id" => id}}) do
|
||||
|> Repo.one()
|
||||
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()
|
||||
def make_like_data(
|
||||
%User{ap_id: ap_id} = actor,
|
||||
@ -286,13 +297,30 @@ def make_like_data(
|
||||
|> maybe_put("id", activity_id)
|
||||
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()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
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 =
|
||||
Map.merge(
|
||||
object.data,
|
||||
%{"#{property}_count" => length(element), "#{property}s" => element}
|
||||
%{"#{property}_count" => length, "#{property}s" => element}
|
||||
)
|
||||
|
||||
object
|
||||
@ -300,6 +328,38 @@ def update_element_in_object(property, element, object) do
|
||||
|> Object.update_and_set_cache()
|
||||
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()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
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()
|
||||
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
|
||||
|
||||
@doc """
|
||||
@ -489,6 +562,25 @@ def make_unlike_data(
|
||||
|> maybe_put("id", activity_id)
|
||||
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()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
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: %{}
|
||||
|
||||
defp build_flag_object(%{account: account, statuses: statuses} = _) do
|
||||
[account.ap_id] ++
|
||||
Enum.map(statuses || [], fn act ->
|
||||
id =
|
||||
case act do
|
||||
%Activity{} = act -> act.data["id"]
|
||||
act when is_map(act) -> act["id"]
|
||||
act when is_binary(act) -> act
|
||||
end
|
||||
[account.ap_id] ++ build_flag_object(%{statuses: statuses})
|
||||
end
|
||||
|
||||
activity = Activity.get_by_ap_id_with_object(id)
|
||||
actor = User.get_by_ap_id(activity.object.data["actor"])
|
||||
defp build_flag_object(%{statuses: statuses}) do
|
||||
Enum.map(statuses || [], &build_flag_object/1)
|
||||
end
|
||||
|
||||
%{
|
||||
"type" => "Note",
|
||||
"id" => activity.data["id"],
|
||||
"content" => activity.object.data["content"],
|
||||
"published" => activity.object.data["published"],
|
||||
"actor" => AccountView.render("show.json", %{user: actor})
|
||||
}
|
||||
end)
|
||||
defp build_flag_object(act) when is_map(act) or is_binary(act) do
|
||||
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)
|
||||
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
|
||||
|
||||
defp build_flag_object(_), do: []
|
||||
@ -679,6 +776,94 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
|
||||
end
|
||||
|
||||
#### 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)
|
||||
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()
|
||||
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 strip_report_status_data(activity) do
|
||||
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.AdminAPI.Config
|
||||
alias Pleroma.Web.AdminAPI.ConfigView
|
||||
@ -639,19 +640,17 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
|
||||
def list_reports(conn, params) do
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
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)
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("index.json", %{reports: Utils.get_reports(params, page, page_size)})
|
||||
end
|
||||
|
||||
reports = ActivityPub.fetch_activities([], params, :offset)
|
||||
def list_grouped_reports(conn, _params) do
|
||||
reports = Utils.get_reported_activities()
|
||||
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("index.json", %{reports: reports})
|
||||
|> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
|
||||
end
|
||||
|
||||
def report_show(conn, %{"id" => id}) do
|
||||
@ -664,17 +663,26 @@ def report_show(conn, %{"id" => id}) do
|
||||
end
|
||||
end
|
||||
|
||||
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
|
||||
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: report
|
||||
})
|
||||
def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
|
||||
result =
|
||||
reports
|
||||
|> Enum.map(fn report ->
|
||||
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "report_update",
|
||||
actor: admin,
|
||||
subject: activity
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("show.json", Report.extract_report_info(report))
|
||||
activity
|
||||
else
|
||||
{: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
|
||||
|
||||
|
@ -42,6 +42,26 @@ def render("show.json", %{report: report, user: user, account: account, statuses
|
||||
}
|
||||
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
|
||||
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||
|
@ -120,6 +120,25 @@ def unfavorite(id_or_ap_id, user) do
|
||||
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
|
||||
with :ok <- validate_not_author(object, user),
|
||||
:ok <- validate_existing_votes(user, object),
|
||||
@ -351,6 +370,13 @@ defp get_reported_account(account_id) do
|
||||
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
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
Utils.update_report_state(activity, state)
|
||||
|
@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||
@relations [:follow, :unfollow]
|
||||
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
|
||||
|
||||
plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
|
||||
plug(RateLimiter, :relations_actions when action in @relations)
|
||||
plug(RateLimiter, :app_account_creation when action == :create)
|
||||
plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
|
||||
plug(RateLimiter, [name: :relations_actions] when action in @relations)
|
||||
plug(RateLimiter, [name: :app_account_creation] when action == :create)
|
||||
plug(:assign_account_by_id when action in @needs_account)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
||||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
|
||||
|
||||
@doc "GET /web/login"
|
||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
||||
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
||||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
plug(RateLimiter, :search when action in [:search, :search2, :account_search])
|
||||
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
|
||||
|
||||
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
||||
accounts = User.search(query, search_options(params, user))
|
||||
|
@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
|
||||
[name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
|
||||
when action in ~w(reblog unreblog)a
|
||||
)
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
|
||||
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
|
||||
when action in ~w(favourite unfavourite)a
|
||||
)
|
||||
|
||||
plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
|
||||
plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
|
@ -12,7 +12,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
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
|
||||
|
||||
def render("participation.json", %{participation: participation, for: user}) do
|
||||
|
@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
plug(RateLimiter, :authentication when action in [:user_exists, :check_password])
|
||||
plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password)
|
||||
plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
|
||||
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
|
||||
|
||||
def user_exists(conn, %{"user" => username}) do
|
||||
with %User{} <- Repo.get_by(User, nickname: username, local: true) do
|
||||
|
@ -46,6 +46,7 @@ def raw_nodeinfo do
|
||||
|
||||
data
|
||||
|> Map.merge(%{quarantined_instances: quarantined})
|
||||
|> Map.put(:enabled, Config.get([:instance, :federating]))
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
||||
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_flash)
|
||||
plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
|
||||
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
|
||||
|
||||
action_fallback(Pleroma.Web.OAuth.FallbackController)
|
||||
|
||||
@ -36,7 +37,7 @@ def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
|
||||
authorize(conn, Map.merge(params, auth_attrs))
|
||||
end
|
||||
|
||||
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
|
||||
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
|
||||
if ControllerHelper.truthy_param?(params["force_login"]) do
|
||||
do_authorize(conn, params)
|
||||
else
|
||||
@ -44,6 +45,22 @@ def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
|
||||
end
|
||||
end
|
||||
|
||||
# Note: the token is set in oauth_plug, but the token and client do not always go together.
|
||||
# For example, MastodonFE's token is set if user requests with another client,
|
||||
# after user already authorized to MastodonFE.
|
||||
# So we have to check client and token.
|
||||
def authorize(
|
||||
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
|
||||
%{"client_id" => client_id} = params
|
||||
) do
|
||||
with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
|
||||
^client_id <- t.app.client_id do
|
||||
handle_existing_authorization(conn, params)
|
||||
else
|
||||
_ -> do_authorize(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
|
||||
|
||||
defp do_authorize(%Plug.Conn{} = conn, params) do
|
||||
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
||||
alias Fallback.RedirectController
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPubController
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
|
||||
RateLimiter,
|
||||
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
||||
when action != :confirmation_resend
|
||||
)
|
||||
|
||||
plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
|
||||
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
|
||||
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
|
||||
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
||||
|
||||
|
@ -7,10 +7,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.ConversationView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
@ -29,6 +34,47 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
||||
|
||||
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
|
||||
with %Participation{} = participation <- Participation.get(participation_id),
|
||||
true <- user.id == participation.user_id do
|
||||
|
@ -180,8 +180,9 @@ defmodule Pleroma.Web.Router do
|
||||
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
||||
|
||||
get("/reports", AdminAPIController, :list_reports)
|
||||
get("/grouped_reports", AdminAPIController, :list_grouped_reports)
|
||||
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)
|
||||
|
||||
put("/statuses/:id", AdminAPIController, :status_update)
|
||||
@ -262,6 +263,12 @@ defmodule Pleroma.Web.Router do
|
||||
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 [] do
|
||||
pipe_through(:authenticated_api)
|
||||
@ -275,6 +282,8 @@ defmodule Pleroma.Web.Router do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
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)
|
||||
|
||||
patch("/accounts/update_avatar", AccountController, :update_avatar)
|
||||
@ -497,6 +506,7 @@ defmodule Pleroma.Web.Router do
|
||||
|
||||
pipeline :ostatus do
|
||||
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
|
||||
plug(Pleroma.Plugs.StaticFEPlug)
|
||||
end
|
||||
|
||||
pipeline :oembed do
|
||||
|
163
lib/pleroma/web/static_fe/static_fe_controller.ex
Normal file
163
lib/pleroma/web/static_fe/static_fe_controller.ex
Normal file
@ -0,0 +1,163 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.StaticFE.StaticFEController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Metadata
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
plug(:put_layout, :static_fe)
|
||||
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
|
||||
plug(:assign_id)
|
||||
|
||||
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
||||
|
||||
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
|
||||
do: name
|
||||
|
||||
defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
|
||||
do: summary
|
||||
|
||||
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
|
||||
%Object{data: data} = Object.normalize(activity)
|
||||
|
||||
%{
|
||||
likes: data["like_count"] || 0,
|
||||
replies: data["repliesCount"] || 0,
|
||||
announces: data["announcement_count"] || 0
|
||||
}
|
||||
end
|
||||
|
||||
def represent(%Activity{} = activity), do: represent(activity, false)
|
||||
|
||||
def represent(%Activity{object: %Object{data: data}} = activity, selected) do
|
||||
{:ok, user} = User.get_or_fetch(activity.object.data["actor"])
|
||||
|
||||
link =
|
||||
case user.local do
|
||||
true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
|
||||
_ -> data["url"] || data["external_url"] || data["id"]
|
||||
end
|
||||
|
||||
%{
|
||||
user: user,
|
||||
title: get_title(activity.object),
|
||||
content: data["content"] || nil,
|
||||
attachment: data["attachment"],
|
||||
link: link,
|
||||
published: data["published"],
|
||||
sensitive: data["sensitive"],
|
||||
selected: selected,
|
||||
counts: get_counts(activity),
|
||||
id: activity.id
|
||||
}
|
||||
end
|
||||
|
||||
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
|
||||
with %Activity{local: true} = activity <-
|
||||
Activity.get_by_id_with_object(notice_id),
|
||||
true <- Visibility.is_public?(activity.object),
|
||||
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
|
||||
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
|
||||
|
||||
timeline =
|
||||
activity.object.data["context"]
|
||||
|> ActivityPub.fetch_activities_for_context(%{})
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
|
||||
|
||||
render(conn, "conversation.html", %{activities: timeline, meta: meta})
|
||||
else
|
||||
%Activity{object: %Object{data: data}} ->
|
||||
conn
|
||||
|> put_status(:found)
|
||||
|> redirect(external: data["url"] || data["external_url"] || data["id"])
|
||||
|
||||
_ ->
|
||||
not_found(conn, "Post not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
|
||||
case User.get_cached_by_nickname_or_id(username_or_id) do
|
||||
%User{} = user ->
|
||||
meta = Metadata.build_tags(%{user: user})
|
||||
|
||||
timeline =
|
||||
ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
|
||||
|> Enum.map(&represent/1)
|
||||
|
||||
prev_page_id =
|
||||
(params["min_id"] || params["max_id"]) &&
|
||||
List.first(timeline) && List.first(timeline).id
|
||||
|
||||
next_page_id = List.last(timeline) && List.last(timeline).id
|
||||
|
||||
render(conn, "profile.html", %{
|
||||
user: user,
|
||||
timeline: timeline,
|
||||
prev_page_id: prev_page_id,
|
||||
next_page_id: next_page_id,
|
||||
meta: meta
|
||||
})
|
||||
|
||||
_ ->
|
||||
not_found(conn, "User not found.")
|
||||
end
|
||||
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
|
||||
|
||||
def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
|
||||
do: assign(conn, :notice_id, notice_id)
|
||||
|
||||
def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
|
||||
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
|
||||
end
|
47
lib/pleroma/web/static_fe/static_fe_view.ex
Normal file
47
lib/pleroma/web/static_fe/static_fe_view.ex
Normal file
@ -0,0 +1,47 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.StaticFE.StaticFEView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Calendar.Strftime
|
||||
alias Pleroma.Emoji.Formatter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Gettext
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
use Phoenix.HTML
|
||||
|
||||
@media_types ["image", "audio", "video"]
|
||||
|
||||
def emoji_for_user(%User{} = user) do
|
||||
user.source_data
|
||||
|> Map.get("tag", [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|
||||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
{String.trim(name, ":"), url}
|
||||
end)
|
||||
end
|
||||
|
||||
def fetch_media_type(%{"mediaType" => mediaType}) do
|
||||
Utils.fetch_media_type(@media_types, mediaType)
|
||||
end
|
||||
|
||||
def format_date(date) do
|
||||
{:ok, date, _} = DateTime.from_iso8601(date)
|
||||
Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
|
||||
end
|
||||
|
||||
def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
|
||||
|
||||
def open_content? do
|
||||
Pleroma.Config.get(
|
||||
[:frontend_configurations, :collapse_message_with_subjects],
|
||||
true
|
||||
)
|
||||
end
|
||||
end
|
15
lib/pleroma/web/templates/layout/static_fe.html.eex
Normal file
15
lib/pleroma/web/templates/layout/static_fe.html.eex
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
||||
<link rel="stylesheet" href="/static/static-fe.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,8 @@
|
||||
<%= case @mediaType do %>
|
||||
<% "audio" -> %>
|
||||
<audio src="<%= @url %>" controls="controls"></audio>
|
||||
<% "video" -> %>
|
||||
<video src="<%= @url %>" controls="controls"></video>
|
||||
<% _ -> %>
|
||||
<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
|
||||
<% end %>
|
@ -0,0 +1,37 @@
|
||||
<div class="activity" <%= if @selected do %> id="selected" <% end %>>
|
||||
<p class="pull-right">
|
||||
<%= link format_date(@published), to: @link, class: "activity-link" %>
|
||||
</p>
|
||||
<%= render("_user_card.html", %{user: @user}) %>
|
||||
<div class="activity-content">
|
||||
<%= if @title != "" do %>
|
||||
<details <%= if open_content?() do %>open<% end %>>
|
||||
<summary><%= raw @title %></summary>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
</details>
|
||||
<% else %>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
<% end %>
|
||||
<%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
|
||||
<%= if @sensitive do %>
|
||||
<details class="nsfw">
|
||||
<summary><%= Gettext.gettext("sensitive media") %></summary>
|
||||
<div>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
</div>
|
||||
</details>
|
||||
<% else %>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @selected do %>
|
||||
<dl class="counts">
|
||||
<dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
|
||||
<dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
|
||||
<dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
<div class="p-author h-card">
|
||||
<a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
|
||||
<div class="avatar">
|
||||
<img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
|
||||
</div>
|
||||
<span class="display-name">
|
||||
<bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
|
||||
<span class="nickname"><%= @user.nickname %></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="conversation">
|
||||
<%= for activity <- @activities do %>
|
||||
<%= render("_notice.html", activity) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
@ -0,0 +1,7 @@
|
||||
<header>
|
||||
<h1><%= gettext("Oops") %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<p><%= @message %></p>
|
||||
</main>
|
@ -0,0 +1,31 @@
|
||||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
|
||||
<h3>
|
||||
<form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
|
||||
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button type="submit" class="collapse">Remote follow</button>
|
||||
</form>
|
||||
<%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
|
||||
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
|
||||
</h3>
|
||||
<p><%= raw @user.bio %></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="activity-stream">
|
||||
<%= for activity <- @timeline do %>
|
||||
<%= render("_notice.html", Map.put(activity, :selected, false)) %>
|
||||
<% end %>
|
||||
<p id="pagination">
|
||||
<%= if @prev_page_id do %>
|
||||
<%= link "«", to: "?min_id=" <> @prev_page_id %>
|
||||
<% end %>
|
||||
<%= if @prev_page_id && @next_page_id, do: " | " %>
|
||||
<%= if @next_page_id do %>
|
||||
<%= link "»", to: "?max_id=" <> @next_page_id %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
1
mix.exs
1
mix.exs
@ -155,7 +155,6 @@ defp deps do
|
||||
{:joken, "~> 2.0"},
|
||||
{:benchee, "~> 1.0"},
|
||||
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
|
||||
{:ex_rated, "~> 1.3"},
|
||||
{:ex_const, "~> 0.2"},
|
||||
{:plug_static_index_html, "~> 1.0.0"},
|
||||
{:excoveralls, "~> 0.11.1", only: :test},
|
||||
|
1
mix.lock
1
mix.lock
@ -33,7 +33,6 @@
|
||||
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "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_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"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"},
|
||||
"fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"},
|
||||
|
@ -28,7 +28,8 @@
|
||||
"oauthRegistrationEndpoint": {
|
||||
"@id": "litepub:oauthRegistrationEndpoint",
|
||||
"@type": "@id"
|
||||
}
|
||||
},
|
||||
"EmojiReaction": "litepub:EmojiReaction"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
BIN
priv/static/static/static-fe.css
Normal file
BIN
priv/static/static/static-fe.css
Normal file
Binary file not shown.
@ -6,6 +6,14 @@ defmodule Pleroma.EmojiTest do
|
||||
use ExUnit.Case, async: true
|
||||
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
|
||||
setup do
|
||||
emoji_list = Emoji.get_all()
|
||||
|
30
test/fixtures/emoji-reaction.json
vendored
Normal file
30
test/fixtures/emoji-reaction.json
vendored
Normal 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
14
test/fixtures/misskey-like.json
vendored
Normal 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"
|
||||
}
|
@ -17,6 +17,16 @@ defmodule Pleroma.Object.ContainmentTest do
|
||||
end
|
||||
|
||||
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
|
||||
data = %{
|
||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||
|
@ -12,163 +12,196 @@ defmodule Pleroma.Plugs.RateLimiterTest do
|
||||
|
||||
# Note: each example must work with separate buckets in order to prevent concurrency issues
|
||||
|
||||
test "init/1" do
|
||||
limiter_name = :test_init
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
|
||||
describe "config" do
|
||||
test "config is required for plug to work" do
|
||||
limiter_name = :test_init
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
|
||||
|
||||
assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name)
|
||||
assert nil == RateLimiter.init(:foo)
|
||||
assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
|
||||
RateLimiter.init(name: limiter_name)
|
||||
|
||||
assert nil == RateLimiter.init(name: :foo)
|
||||
end
|
||||
|
||||
test "it restricts based on config values" do
|
||||
limiter_name = :test_opts
|
||||
scale = 80
|
||||
limit = 5
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
|
||||
|
||||
opts = RateLimiter.init(name: limiter_name)
|
||||
conn = conn(:get, "/")
|
||||
|
||||
for i <- 1..5 do
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
Process.sleep(10)
|
||||
end
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
Process.sleep(50)
|
||||
|
||||
conn = conn(:get, "/")
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
|
||||
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn.resp_body
|
||||
refute conn.halted
|
||||
end
|
||||
end
|
||||
|
||||
test "ip/1" do
|
||||
assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}})
|
||||
describe "options" do
|
||||
test "`bucket_name` option overrides default bucket name" do
|
||||
limiter_name = :test_bucket_name
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
|
||||
|
||||
base_bucket_name = "#{limiter_name}:group1"
|
||||
opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
|
||||
|
||||
conn = conn(:get, "/")
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
|
||||
assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
end
|
||||
|
||||
test "`params` option allows different queries to be tracked independently" do
|
||||
limiter_name = :test_params
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
|
||||
|
||||
opts = RateLimiter.init(name: limiter_name, params: ["id"])
|
||||
|
||||
conn = conn(:get, "/?id=1")
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
conn_2 = conn(:get, "/?id=2")
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
|
||||
end
|
||||
|
||||
test "it supports combination of options modifying bucket name" do
|
||||
limiter_name = :test_options_combo
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
|
||||
|
||||
base_bucket_name = "#{limiter_name}:group1"
|
||||
opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
|
||||
id = "100"
|
||||
|
||||
conn = conn(:get, "/?id=#{id}")
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
conn_2 = conn(:get, "/?id=#{101}")
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
|
||||
assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
test "it restricts by opts" do
|
||||
limiter_name = :test_opts
|
||||
scale = 1000
|
||||
limit = 5
|
||||
describe "unauthenticated users" do
|
||||
test "are restricted based on remote IP" do
|
||||
limiter_name = :test_unauthenticated
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
|
||||
opts = RateLimiter.init(name: limiter_name)
|
||||
|
||||
opts = RateLimiter.init(limiter_name)
|
||||
conn = conn(:get, "/")
|
||||
bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
|
||||
conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
|
||||
conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
for i <- 1..5 do
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
conn_2 = RateLimiter.call(conn_2, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
Process.sleep(to_reset)
|
||||
|
||||
conn = conn(:get, "/")
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn.resp_body
|
||||
refute conn.halted
|
||||
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn_2.resp_body
|
||||
refute conn_2.halted
|
||||
end
|
||||
end
|
||||
|
||||
test "`bucket_name` option overrides default bucket name" do
|
||||
limiter_name = :test_bucket_name
|
||||
scale = 1000
|
||||
limit = 5
|
||||
describe "authenticated users" do
|
||||
setup do
|
||||
Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
|
||||
base_bucket_name = "#{limiter_name}:group1"
|
||||
opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
|
||||
:ok
|
||||
end
|
||||
|
||||
conn = conn(:get, "/")
|
||||
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
|
||||
customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
|
||||
test "can have limits seperate from unauthenticated connections" do
|
||||
limiter_name = :test_authenticated
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit)
|
||||
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
|
||||
end
|
||||
scale = 1000
|
||||
limit = 5
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
|
||||
|
||||
test "`params` option appends specified params' values to bucket name" do
|
||||
limiter_name = :test_params
|
||||
scale = 1000
|
||||
limit = 5
|
||||
opts = RateLimiter.init(name: limiter_name)
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
|
||||
opts = RateLimiter.init({limiter_name, params: ["id"]})
|
||||
id = "1"
|
||||
user = insert(:user)
|
||||
conn = conn(:get, "/") |> assign(:user, user)
|
||||
|
||||
conn = conn(:get, "/?id=#{id}")
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
for i <- 1..5 do
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
|
||||
parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
|
||||
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
|
||||
end
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
test "it supports combination of options modifying bucket name" do
|
||||
limiter_name = :test_options_combo
|
||||
scale = 1000
|
||||
limit = 5
|
||||
Process.sleep(1550)
|
||||
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
|
||||
base_bucket_name = "#{limiter_name}:group1"
|
||||
opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]})
|
||||
id = "100"
|
||||
conn = conn(:get, "/") |> assign(:user, user)
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
|
||||
conn = conn(:get, "/?id=#{id}")
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn.resp_body
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
|
||||
parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}"
|
||||
test "diffrerent users are counted independently" do
|
||||
limiter_name = :test_authenticated
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
|
||||
|
||||
RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
|
||||
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
|
||||
end
|
||||
opts = RateLimiter.init(name: limiter_name)
|
||||
|
||||
test "optional limits for authenticated users" do
|
||||
limiter_name = :test_authenticated
|
||||
Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
|
||||
user = insert(:user)
|
||||
conn = conn(:get, "/") |> assign(:user, user)
|
||||
|
||||
scale = 1000
|
||||
limit = 5
|
||||
Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
|
||||
user_2 = insert(:user)
|
||||
conn_2 = conn(:get, "/") |> assign(:user, user_2)
|
||||
|
||||
opts = RateLimiter.init(limiter_name)
|
||||
for i <- 1..5 do
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
|
||||
end
|
||||
|
||||
user = insert(:user)
|
||||
conn = conn(:get, "/") |> assign(:user, user)
|
||||
bucket_name = "#{limiter_name}:#{user.id}"
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
|
||||
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
|
||||
assert conn.halted
|
||||
|
||||
Process.sleep(to_reset)
|
||||
|
||||
conn = conn(:get, "/") |> assign(:user, user)
|
||||
|
||||
conn = RateLimiter.call(conn, opts)
|
||||
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||
|
||||
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn.resp_body
|
||||
refute conn.halted
|
||||
conn_2 = RateLimiter.call(conn_2, opts)
|
||||
assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
|
||||
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
|
||||
refute conn_2.resp_body
|
||||
refute conn_2.halted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -812,6 +812,78 @@ test "returns reblogs for users for whom reblogs have not been muted" do
|
||||
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
|
||||
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
|
||||
Pleroma.Config.put([:instance, :federating], true)
|
||||
|
@ -339,6 +339,80 @@ test "it works for incoming likes" do
|
||||
assert data["object"] == activity.data["object"]
|
||||
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
|
||||
user = insert(:user)
|
||||
{: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")
|
||||
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
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
|
||||
|
||||
|
@ -636,4 +636,47 @@ test "removes actor from announcements" do
|
||||
assert updated_object.data["announcement_count"] == 1
|
||||
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
|
||||
|
@ -1312,7 +1312,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/pleroma/admin/reports/:id" do
|
||||
describe "PATCH /api/pleroma/admin/reports" do
|
||||
setup %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
[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]
|
||||
})
|
||||
|
||||
%{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
|
||||
|
||||
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|
||||
|> json_response(:ok)
|
||||
conn
|
||||
|> patch("/api/pleroma/admin/reports", %{
|
||||
"reports" => [
|
||||
%{"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)
|
||||
|
||||
@ -1343,12 +1359,16 @@ test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
|
||||
end
|
||||
|
||||
test "closes report", %{conn: conn, id: id, admin: admin} do
|
||||
response =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|
||||
|> json_response(:ok)
|
||||
conn
|
||||
|> patch("/api/pleroma/admin/reports", %{
|
||||
"reports" => [
|
||||
%{"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)
|
||||
|
||||
@ -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
|
||||
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
|
||||
|
||||
test "returns 404 when report is not exist", %{conn: conn} do
|
||||
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
|
||||
|
||||
@ -1492,7 +1549,145 @@ test "returns 403 when requested by anonymous" do
|
||||
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
|
||||
setup %{conn: conn} do
|
||||
admin = insert(:user, is_admin: true)
|
||||
@ -2269,6 +2464,10 @@ test "delete part of settings by atom subkeys", %{conn: conn} do
|
||||
Pleroma.Config.put([:instance, :dynamic_configuration], true)
|
||||
end
|
||||
|
||||
clear_config([:feed, :post_title]) do
|
||||
Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"})
|
||||
end
|
||||
|
||||
test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
|
||||
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
|
||||
conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
|
||||
|
@ -227,6 +227,33 @@ test "it can handle activities that expire" do
|
||||
end
|
||||
|
||||
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
|
||||
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"}
|
||||
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
|
||||
|
||||
describe "reblog muting" do
|
||||
|
@ -84,6 +84,30 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
|
||||
Pleroma.Config.put([:instance, :safe_dm_mentions], option)
|
||||
end
|
||||
|
||||
test "it shows if federation is enabled/disabled", %{conn: conn} do
|
||||
original = Pleroma.Config.get([:instance, :federating])
|
||||
|
||||
Pleroma.Config.put([:instance, :federating], true)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/nodeinfo/2.1.json")
|
||||
|> json_response(:ok)
|
||||
|
||||
assert response["metadata"]["federation"]["enabled"] == true
|
||||
|
||||
Pleroma.Config.put([:instance, :federating], false)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/nodeinfo/2.1.json")
|
||||
|> json_response(:ok)
|
||||
|
||||
assert response["metadata"]["federation"]["enabled"] == false
|
||||
|
||||
Pleroma.Config.put([:instance, :federating], original)
|
||||
end
|
||||
|
||||
test "it shows MRF transparency data if enabled", %{conn: conn} do
|
||||
config = Pleroma.Config.get([:instance, :rewrite_policy])
|
||||
Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
|
@ -469,6 +469,29 @@ test "renders authentication page if user is already authenticated but `force_lo
|
||||
assert html_response(conn, 200) =~ ~s(type="submit")
|
||||
end
|
||||
|
||||
test "renders authentication page if user is already authenticated but user request with another client",
|
||||
%{
|
||||
app: app,
|
||||
conn: conn
|
||||
} do
|
||||
token = insert(:oauth_token, app_id: app.id)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:oauth_token, token.token)
|
||||
|> get(
|
||||
"/oauth/authorize",
|
||||
%{
|
||||
"response_type" => "code",
|
||||
"client_id" => "another_client_id",
|
||||
"redirect_uri" => OAuthController.default_redirect_uri(app),
|
||||
"scope" => "read"
|
||||
}
|
||||
)
|
||||
|
||||
assert html_response(conn, 200) =~ ~s(type="submit")
|
||||
end
|
||||
|
||||
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
|
||||
%{
|
||||
app: app,
|
||||
|
@ -7,12 +7,72 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
|
||||
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
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
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
210
test/web/static_fe/static_fe_controller_test.exs
Normal file
210
test/web/static_fe/static_fe_controller_test.exs
Normal file
@ -0,0 +1,210 @@
|
||||
defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
clear_config_all([:static_fe, :enabled]) do
|
||||
Pleroma.Config.put([:static_fe, :enabled], true)
|
||||
end
|
||||
|
||||
describe "user profile page" do
|
||||
test "just the profile as HTML", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/users/#{user.nickname}")
|
||||
|
||||
assert html_response(conn, 200) =~ user.nickname
|
||||
end
|
||||
|
||||
test "renders json unless there's an html accept header", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get("/users/#{user.nickname}")
|
||||
|
||||
assert json_response(conn, 200)
|
||||
end
|
||||
|
||||
test "404 when user not found", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/users/limpopo")
|
||||
|
||||
assert html_response(conn, 404) =~ "not found"
|
||||
end
|
||||
|
||||
test "profile does not include private messages", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
CommonAPI.post(user, %{"status" => "public"})
|
||||
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/users/#{user.nickname}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">public<"
|
||||
refute html =~ ">private<"
|
||||
end
|
||||
|
||||
test "pagination", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/users/#{user.nickname}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">test30<"
|
||||
assert html =~ ">test11<"
|
||||
refute html =~ ">test10<"
|
||||
refute html =~ ">test1<"
|
||||
end
|
||||
|
||||
test "pagination, page 2", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
|
||||
{:ok, a11} = Enum.at(activities, 11)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/users/#{user.nickname}?max_id=#{a11.id}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
|
||||
assert html =~ ">test1<"
|
||||
assert html =~ ">test10<"
|
||||
refute html =~ ">test20<"
|
||||
refute html =~ ">test29<"
|
||||
end
|
||||
end
|
||||
|
||||
describe "notice rendering" do
|
||||
test "single notice page", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/notice/#{activity.id}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
assert html =~ "<header>"
|
||||
assert html =~ user.nickname
|
||||
assert html =~ "testing a thing!"
|
||||
end
|
||||
|
||||
test "shows the whole thread", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"})
|
||||
|
||||
CommonAPI.post(user, %{
|
||||
"status" => "these are the voyages or something",
|
||||
"in_reply_to_status_id" => activity.id
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/notice/#{activity.id}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
assert html =~ "the final frontier"
|
||||
assert html =~ "voyages"
|
||||
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
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/notice/88c9c317")
|
||||
|
||||
assert html_response(conn, 404) =~ "not found"
|
||||
end
|
||||
|
||||
test "404 for private status", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/notice/#{activity.id}")
|
||||
|
||||
assert html_response(conn, 404) =~ "not found"
|
||||
end
|
||||
|
||||
test "302 for remote cached status", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"to" => user.follower_address,
|
||||
"cc" => "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"content" => "blah blah blah",
|
||||
"type" => "Note",
|
||||
"attributedTo" => user.ap_id,
|
||||
"inReplyTo" => nil
|
||||
},
|
||||
"actor" => user.ap_id
|
||||
}
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "text/html")
|
||||
|> get("/notice/#{activity.id}")
|
||||
|
||||
assert html_response(conn, 302) =~ "redirected"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user