diff options
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 |