summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--CHANGELOG.md8
-rw-r--r--docs/API/admin_api.md196
-rw-r--r--docs/API/pleroma_api.md32
-rw-r--r--docs/installation/openbsd_en.md38
-rw-r--r--lib/pleroma/activity.ex17
-rw-r--r--lib/pleroma/constants.ex2
-rw-r--r--lib/pleroma/emoji-data.txt769
-rw-r--r--lib/pleroma/emoji.ex31
-rw-r--r--lib/pleroma/object/containment.ex2
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex26
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex73
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex235
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex46
-rw-r--r--lib/pleroma/web/admin_api/views/report_view.ex20
-rw-r--r--lib/pleroma/web/common_api/common_api.ex26
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex46
-rw-r--r--lib/pleroma/web/router.ex11
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex51
-rw-r--r--priv/static/schemas/litepub-0.1.jsonld3
-rw-r--r--test/emoji_test.exs8
-rw-r--r--test/fixtures/emoji-reaction.json30
-rw-r--r--test/fixtures/misskey-like.json14
-rw-r--r--test/object/containment_test.exs10
-rw-r--r--test/plugs/rate_limiter_test.exs2
-rw-r--r--test/web/activity_pub/activity_pub_test.exs72
-rw-r--r--test/web/activity_pub/transmogrifier_test.exs88
-rw-r--r--test/web/activity_pub/utils_test.exs43
-rw-r--r--test/web/admin_api/admin_api_controller_test.exs229
-rw-r--r--test/web/common_api/common_api_test.exs56
-rw-r--r--test/web/pleroma_api/controllers/pleroma_api_controller_test.exs60
-rw-r--r--test/web/static_fe/static_fe_controller_test.exs33
32 files changed, 2120 insertions, 161 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d915ebae9..ab62c8827 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -53,7 +53,7 @@ unit-testing:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- - mix coveralls --trace --preload-modules
+ - mix coveralls --preload-modules
unit-testing-rum:
stage: test
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 945e7dc4d..b4ad91b0d 100644
--- a/CHANGELOG.md
+++ b/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`)
@@ -45,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<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
@@ -53,14 +55,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
- 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
</details>
### Fixed
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index c042b08ac..9d914c9a6 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -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
diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md
index 6c326dc9b..ad16d027e 100644
--- a/docs/API/pleroma_api.md
+++ b/docs/API/pleroma_api.md
@@ -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..."}]
+}
+```
diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md
index 3585a326b..45602bd75 100644
--- a/docs/installation/openbsd_en.md
+++ b/docs/installation/openbsd_en.md
@@ -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
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index c1065611b..7e283df32 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -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 @@ defmodule Pleroma.Activity 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,
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index 0bf20cdd0..1a432e681 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -9,6 +9,8 @@ defmodule Pleroma.Constants do
const(object_internal_fields,
do: [
+ "reactions",
+ "reaction_count",
"likes",
"like_count",
"announcements",
diff --git a/lib/pleroma/emoji-data.txt b/lib/pleroma/emoji-data.txt
new file mode 100644
index 000000000..2fb5c3ff6
--- /dev/null
+++ b/lib/pleroma/emoji-data.txt
@@ -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
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index bafad2ae9..abfd49aaa 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -98,4 +98,35 @@ defmodule Pleroma.Emoji 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
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index a1f9c1250..25aa32f60 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -64,6 +64,8 @@ defmodule Pleroma.Object.Containment 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)
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 65dd251f3..d0c014e9d 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -322,6 +322,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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,
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 91a164eff..15612545b 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -566,6 +566,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
@@ -581,6 +609,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
) do
@@ -718,6 +767,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
@@ -1048,7 +1119,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index d812fd734..c45662359 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
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
+
+ defp build_flag_object(%{statuses: statuses}) do
+ Enum.map(statuses || [], &build_flag_object/1)
+ end
- activity = Activity.get_by_ap_id_with_object(id)
- actor = User.get_by_ap_id(activity.object.data["actor"])
+ 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
- %{
- "type" => "Note",
- "id" => activity.data["id"],
- "content" => activity.object.data["content"],
- "published" => activity.object.data["published"],
- "actor" => AccountView.render("show.json", %{user: actor})
- }
- 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> 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
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 30fc01755..8c1318d1b 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -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
@@ -624,19 +625,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
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
@@ -649,17 +648,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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
+ })
+
+ activity
+ else
+ {:error, message} -> %{id: report["id"], error: message}
+ end
+ end)
- conn
- |> put_view(ReportView)
- |> render("show.json", Report.extract_report_info(report))
+ 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
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 101a74c63..ca88595c7 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -42,6 +42,26 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}
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}))
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index e57345621..fe6e26a90 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -120,6 +120,25 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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)
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index 651a99423..8fed3f5bb 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index ecf5f744c..9b8b373b8 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -178,8 +178,9 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/statuses", AdminAPIController, :list_user_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)
@@ -261,6 +262,12 @@ defmodule Pleroma.Web.Router do
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)
@@ -273,6 +280,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)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index 5e60c82b0..8ccf15f4b 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -27,6 +27,12 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
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)
@@ -77,10 +83,13 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
render(conn, "conversation.html", %{activities: timeline, meta: meta})
else
- _ ->
+ %Activity{object: %Object{data: data}} ->
conn
- |> put_status(404)
- |> render("error.html", %{message: "Post not found.", meta: ""})
+ |> put_status(:found)
+ |> redirect(external: data["url"] || data["external_url"] || data["id"])
+
+ _ ->
+ not_found(conn, "Post not found.")
end
end
@@ -108,9 +117,33 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
})
_ ->
- conn
- |> put_status(404)
- |> render("error.html", %{message: "User not found.", 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
@@ -120,5 +153,11 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
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
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index c8e69cab5..8bae42f6d 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -28,7 +28,8 @@
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
- }
+ },
+ "EmojiReaction": "litepub:EmojiReaction"
}
]
}
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 1fdbd0fdf..7bdf2b6fa 100644
--- a/test/emoji_test.exs
+++ b/test/emoji_test.exs
@@ -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()
diff --git a/test/fixtures/emoji-reaction.json b/test/fixtures/emoji-reaction.json
new file mode 100644
index 000000000..3812e43ad
--- /dev/null
+++ b/test/fixtures/emoji-reaction.json
@@ -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"
+ }
+ ]
+}
diff --git a/test/fixtures/misskey-like.json b/test/fixtures/misskey-like.json
new file mode 100644
index 000000000..84d56f473
--- /dev/null
+++ b/test/fixtures/misskey-like.json
@@ -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"
+}
diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs
index 71fe5204c..7636803a6 100644
--- a/test/object/containment_test.exs
+++ b/test/object/containment_test.exs
@@ -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"
diff --git a/test/plugs/rate_limiter_test.exs b/test/plugs/rate_limiter_test.exs
index bacd621e1..49f63c424 100644
--- a/test/plugs/rate_limiter_test.exs
+++ b/test/plugs/rate_limiter_test.exs
@@ -25,7 +25,7 @@ defmodule Pleroma.Plugs.RateLimiterTest do
test "it restricts based on config values" do
limiter_name = :test_opts
- scale = 60
+ scale = 80
limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 0d0281faf..d437ad456 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -812,6 +812,78 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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)
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 4645eb39d..0bdd514e9 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -339,6 +339,80 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest 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!()
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index 586eb1d2f..1feb076ba 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -636,4 +636,47 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest 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
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index bc9235309..3a4c4d65c 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1312,7 +1312,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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)
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 8e6fbd7f0..138488d44 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -227,6 +227,33 @@ defmodule Pleroma.Web.CommonAPITest 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 @@ defmodule Pleroma.Web.CommonAPITest 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
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index 0c83edb56..b1b59beed 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -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)
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
index effdfbeb3..2ce8f9fa3 100644
--- a/test/web/static_fe/static_fe_controller_test.exs
+++ b/test/web/static_fe/static_fe_controller_test.exs
@@ -1,5 +1,6 @@
defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase
+ alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
@@ -128,6 +129,34 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
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
@@ -151,7 +180,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
assert html_response(conn, 404) =~ "not found"
end
- test "404 for remote cached status", %{conn: conn} do
+ test "302 for remote cached status", %{conn: conn} do
user = insert(:user)
message = %{
@@ -175,7 +204,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
- assert html_response(conn, 404) =~ "not found"
+ assert html_response(conn, 302) =~ "redirected"
end
end
end