forked from AkkomaGang/akkoma
Merge branch 'develop' into issue/1276
This commit is contained in:
commit
49bb0a130f
157 changed files with 6148 additions and 1625 deletions
.dockerignore.gitlab-ci.ymlCHANGELOG.mdREADME.mdmix.exsmix.lock
benchmarks
config
docs
API
administration/CLI_tasks
configuration
installation
lib
mix
pleroma
activity.exapplication.exconstants.ex
conversation
docs
emoji-data.txtemoji.exfollowing_relationship.exmoderation_log.exnotification.exobject.exobject
plugs
admin_secret_authentication_plug.exoauth_plug.exrate_limiter.ex
user.exrate_limiter
static_fe_plug.extrailing_format_plug.exuser_enabled_plug.exuser
web
activity_pub
admin_api
common_api
endpoint.exfeed
mastodon_api
controllers
account_controller.exauth_controller.exsearch_controller.exstatus_controller.extimeline_controller.ex
views
mongooseim
nodeinfo
oauth
ostatus
pleroma_api/controllers
push
router.exstatic_fe
templates
workers
priv
|
@ -5,7 +5,6 @@ CC-BY-SA-4.0
|
|||
COPYING
|
||||
*file
|
||||
elixir_buildpack.config
|
||||
docs/
|
||||
test/
|
||||
|
||||
# Required to get version
|
||||
|
|
|
@ -29,28 +29,13 @@ build:
|
|||
- mix deps.get
|
||||
- mix compile --force
|
||||
|
||||
docs-build:
|
||||
stage: build
|
||||
only:
|
||||
- master@pleroma/pleroma
|
||||
- develop@pleroma/pleroma
|
||||
variables:
|
||||
MIX_ENV: dev
|
||||
PLEROMA_BUILD_ENV: prod
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix compile
|
||||
- mix docs
|
||||
artifacts:
|
||||
paths:
|
||||
- priv/static/doc
|
||||
|
||||
benchmark:
|
||||
stage: benchmark
|
||||
when: manual
|
||||
variables:
|
||||
MIX_ENV: benchmark
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: postgres:9.6
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
|
@ -62,19 +47,32 @@ benchmark:
|
|||
unit-testing:
|
||||
stage: test
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: postgres:9.6
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mix coveralls --trace --preload-modules
|
||||
- mix coveralls --preload-modules
|
||||
|
||||
federated-testing:
|
||||
stage: test
|
||||
services:
|
||||
- name: minibikini/postgres-with-rum:12
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- epmd -daemon
|
||||
- mix test --trace --only federated
|
||||
|
||||
unit-testing-rum:
|
||||
stage: test
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
- name: minibikini/postgres-with-rum:12
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
variables:
|
||||
|
@ -84,7 +82,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
|
||||
|
@ -129,6 +127,7 @@ review_app:
|
|||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
|
||||
- ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
|
||||
- (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true
|
||||
- (ssh -t dokku@pleroma.online -- git:set "$CI_ENVIRONMENT_SLUG" keep-git-dir true) || true
|
||||
- ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku
|
||||
- (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true
|
||||
- (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
|
||||
|
|
74
CHANGELOG.md
74
CHANGELOG.md
|
@ -10,20 +10,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
### Changed
|
||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||
- **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default
|
||||
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
|
||||
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
|
||||
- Enabled `:instance, extended_nickname_format` in the default config
|
||||
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
|
||||
- Extract RSS functionality from OStatus
|
||||
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
|
||||
- OStatus: Extract RSS functionality
|
||||
- Deprecated `User.Info` embedded schema (fields moved to `User`)
|
||||
- Store status data inside Flag activity
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- **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`)
|
||||
- Admin API: Return link alongside with token on password reset
|
||||
- Admin API: Support authentication via `x-admin-token` HTTP header
|
||||
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
|
||||
- Mastodon API: `pleroma.thread_muted` to the Status entity
|
||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||
|
@ -34,12 +41,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
### Added
|
||||
- Refreshing poll results for remote polls
|
||||
- Authentication: Added rate limit for password-authorized actions / login existence checks
|
||||
- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
|
||||
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
|
||||
- Mix task to list all users (`mix pleroma.user list`)
|
||||
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
|
||||
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
|
||||
<details>
|
||||
<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
|
||||
|
@ -48,38 +59,69 @@ 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
|
||||
- ActivityPub: Support `Move` activities
|
||||
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers
|
||||
|
||||
### Changed
|
||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||
- **Breaking:** Admin API: Return link alongside with token on password reset
|
||||
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
|
||||
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
|
||||
- Admin API: Return `total` when querying for reports
|
||||
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
|
||||
- Admin API: Return link alongside with token on password reset
|
||||
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
|
||||
- OStatus: Extract RSS functionality
|
||||
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
|
||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||
- Deprecated `User.Info` embedded schema (fields moved to `User`)
|
||||
- Store status data inside Flag activity
|
||||
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
|
||||
- Configuration: `feed` option for user atom feed.
|
||||
- Pleroma API: Add Emoji reactions
|
||||
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
|
||||
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
|
||||
</details>
|
||||
|
||||
### Fixed
|
||||
- Report emails now include functional links to profiles of remote user accounts
|
||||
- Not being able to log in to some third-party apps when logged in to MastoFE
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
|
||||
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
|
||||
- Admin API: Error when trying to update reports in the "old" format
|
||||
</details>
|
||||
|
||||
## [1.1.6] - 2019-11-19
|
||||
### Fixed
|
||||
- Not being able to log into to third party apps when the browser is logged into mastofe
|
||||
- Email confirmation not being required even when enabled
|
||||
- Mastodon API: conversations API crashing when one status is malformed
|
||||
|
||||
### Bundled Pleroma-FE Changes
|
||||
#### Added
|
||||
- About page
|
||||
- Meme arrows
|
||||
|
||||
#### Fixed
|
||||
- Image modal not closing unless clicked outside of image
|
||||
- Attachment upload spinner not being centered
|
||||
- Showing follow counters being 0 when they are actually hidden
|
||||
|
||||
## [1.1.5] - 2019-11-09
|
||||
### Fixed
|
||||
- Polls having different numbers in timelines/notifications/poll api endpoints due to cache desyncronization
|
||||
- Pleroma API: OAuth token endpoint not being found when ".json" suffix is appended
|
||||
|
||||
### Changed
|
||||
- Frontend bundle updated to [044c9ad0](https://git.pleroma.social/pleroma/pleroma-fe/commit/044c9ad0562af059dd961d50961a3880fca9c642)
|
||||
|
||||
## [1.1.4] - 2019-11-01
|
||||
### Fixed
|
||||
- Added a migration that fills up empty user.info fields to prevent breakage after previous unsafe migrations.
|
||||
- Failure to migrate from pre-1.0.0 versions
|
||||
- Mastodon API: Notification stream not including follow notifications
|
||||
|
||||
## [1.1.3] - 2019-10-25
|
||||
### Fixed
|
||||
- Blocked users showing up in notifications collapsed as if they were muted
|
||||
- `pleroma_ctl` not working on Debian's default shell
|
||||
|
||||
## [1.1.2] - 2019-10-18
|
||||
### Fixed
|
||||
- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.
|
||||
|
|
95
README.md
95
README.md
|
@ -1,80 +1,43 @@
|
|||
# Pleroma
|
||||
<img src="https://git.pleroma.social/pleroma/pleroma/uploads/8cec84f5a084d887339f57deeb8a293e/pleroma-banner-vector-nopad-notext.svg" width="300px" />
|
||||
|
||||
**Note**: This readme as well as complete documentation is also available at <https://docs-develop.pleroma.social>
|
||||
## About
|
||||
|
||||
## About Pleroma
|
||||
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support ActivityPub. What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
|
||||
|
||||
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
|
||||
Pleroma is written in Elixir and uses PostgresSQL for data storage. It's efficient enough to be ran on low-power devices like Raspberry Pi (though we wouldn't recommend storing the database on the internal SD card ;) but can scale well when ran on more powerful hardware (albeit only single-node for now).
|
||||
|
||||
Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.
|
||||
For clients it supports the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/) with Pleroma extensions (see the API section on <https://docs-develop.pleroma.social>).
|
||||
|
||||
For clients it supports the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/) with Pleroma extensions (see "Pleroma's APIs and Mastodon API extensions" section on <https://docs-develop.pleroma.social>).
|
||||
|
||||
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)
|
||||
|
||||
If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
|
||||
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/backend/clients/)
|
||||
|
||||
## Installation
|
||||
**Note:** The guide below may be outdated and in most cases shouldn't be used. Instead check out our [wiki](https://docs.pleroma.social) for platform-specific installation instructions, most likely [Installing on Linux using OTP releases](https://docs.pleroma.social/otp_en.html) is the guide you need.
|
||||
|
||||
### OTP releases (Recommended)
|
||||
If you are running Linux (glibc or musl) on x86/arm, the recommended way to install Pleroma is by using OTP releases. OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it. The installation instructions are available [here](https://docs-develop.pleroma.social/backend/installation/otp_en/).
|
||||
|
||||
### From Source
|
||||
If your platform is not supported, or you just want to be able to edit the source code easily, you may install Pleroma from source.
|
||||
|
||||
- [Debian-based](https://docs-develop.pleroma.social/backend/installation/debian_based_en/)
|
||||
- [Debian-based (jp)](https://docs-develop.pleroma.social/backend/installation/debian_based_jp/)
|
||||
- [Alpine Linux](https://docs-develop.pleroma.social/backend/installation/alpine_linux_en/)
|
||||
- [Arch Linux](https://docs-develop.pleroma.social/backend/installation/arch_linux_en/)
|
||||
- [Gentoo Linux](https://docs-develop.pleroma.social/backend/installation/gentoo_en/)
|
||||
- [NetBSD](https://docs-develop.pleroma.social/backend/installation/netbsd_en/)
|
||||
- [OpenBSD](https://docs-develop.pleroma.social/backend/installation/openbsd_en/)
|
||||
- [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/)
|
||||
- [CentOS 7](https://docs-develop.pleroma.social/backend/installation/centos7_en/)
|
||||
|
||||
### OS/Distro packages
|
||||
Currently Pleroma is not packaged by any OS/Distros, but feel free to reach out to us at [#pleroma-dev on freenode](https://webchat.freenode.net/?channels=%23pleroma-dev) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma-dev:matrix.org> for assistance. If you want to change default options in your Pleroma package, please **discuss it with us first**.
|
||||
Currently Pleroma is not packaged by any OS/Distros, but if you want to package it for one, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**.
|
||||
|
||||
### Docker
|
||||
While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
|
||||
|
||||
### Dependencies
|
||||
## Documentation
|
||||
- Latest Released revision: <https://docs.pleroma.social>
|
||||
- Latest Git revision: <https://docs-develop.pleroma.social>
|
||||
|
||||
* Postgresql version 9.6 or newer, including the contrib modules
|
||||
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir’s install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
|
||||
* Build-essential tools
|
||||
|
||||
### Configuration
|
||||
|
||||
* Run `mix deps.get` to install elixir dependencies.
|
||||
* Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step.
|
||||
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [`docs/config.md`](docs/config.md) in the repository, or at the "Configuration" page on <https://docs-develop.pleroma.social/config.html>
|
||||
* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
|
||||
* You can check if your instance is configured correctly by running it with `mix phx.server` and checking the instance info endpoint at `/api/v1/instance`. If it shows your uri, name and email correctly, you are configured correctly. If it shows something like `localhost:4000`, your configuration is probably wrong, unless you are running a local development setup.
|
||||
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: <https://letsencrypt.org/>. The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically.
|
||||
|
||||
## Running
|
||||
|
||||
* By default, it listens on port 4000 (TCP), so you can access it on <http://localhost:4000/> (if you are on the same machine). In case of an error it will restart automatically.
|
||||
|
||||
### Frontends
|
||||
|
||||
Pleroma comes with two frontends. The first one, Pleroma FE, can be reached by normally visiting the site. The other one, based on the Mastodon project, can be found by visiting the /web path of your site.
|
||||
|
||||
### As systemd service (with provided .service file)
|
||||
|
||||
Example .service file can be found in `installation/pleroma.service`. Copy this to `/etc/systemd/system/`. Running `systemctl enable --now pleroma.service` will run Pleroma and enable startup on boot. Logs can be watched by using `journalctl -fu pleroma.service`.
|
||||
|
||||
### As OpenRC service (with provided RC file)
|
||||
|
||||
Copy `installation/init.d/pleroma` to `/etc/init.d/pleroma`. You can add it to the services ran by default with: `rc-update add pleroma`
|
||||
|
||||
### Standalone/run by other means
|
||||
|
||||
Run `mix phx.server` in repository’s root, it will output log into stdout/stderr.
|
||||
|
||||
### Using an upstream proxy for federation
|
||||
|
||||
Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that Pleroma makes to an upstream proxy server:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :http,
|
||||
proxy_url: "127.0.0.1:8123"
|
||||
```
|
||||
|
||||
This is useful for running Pleroma inside Tor or I2P.
|
||||
|
||||
## Customization and contribution
|
||||
|
||||
The [Pleroma Documentation](https://docs-develop.pleroma.social) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No incoming federation
|
||||
|
||||
Check that you correctly forward the `host` header to the backend. It is needed to validate signatures.
|
||||
## Community Channels
|
||||
* IRC: **#pleroma** and **#pleroma-dev** on freenode, webchat is available at <https://irc.pleroma.social>
|
||||
* Matrix: <https://matrix.to/#/#freenode_#pleroma:matrix.org> and <https://matrix.to/#/#freenode_#pleroma-dev:matrix.org>
|
||||
|
|
|
@ -95,7 +95,36 @@ def query_timelines(user) do
|
|||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
end,
|
||||
"Rendering favorites timeline" => fn ->
|
||||
conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil)
|
||||
Pleroma.Web.MastodonAPI.StatusController.favourites(
|
||||
%Plug.Conn{conn |
|
||||
assigns: %{user: user},
|
||||
query_params: %{"limit" => "0"},
|
||||
body_params: %{},
|
||||
cookies: %{},
|
||||
params: %{},
|
||||
path_params: %{},
|
||||
private: %{
|
||||
Pleroma.Web.Router => {[], %{}},
|
||||
phoenix_router: Pleroma.Web.Router,
|
||||
phoenix_action: :favourites,
|
||||
phoenix_controller: Pleroma.Web.MastodonAPI.StatusController,
|
||||
phoenix_endpoint: Pleroma.Web.Endpoint,
|
||||
phoenix_format: "json",
|
||||
phoenix_layout: {Pleroma.Web.LayoutView, "app.html"},
|
||||
phoenix_recycled: true,
|
||||
|
||||
phoenix_view: Pleroma.Web.MastodonAPI.StatusView,
|
||||
plug_session: %{"user_id" => user.id},
|
||||
plug_session_fetch: :done,
|
||||
plug_session_info: :write,
|
||||
plug_skip_csrf_protection: true
|
||||
}
|
||||
},
|
||||
%{})
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,24 @@ defmodule Pleroma.LoadTesting.Generator do
|
|||
use Pleroma.LoadTesting.Helper
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
def generate_like_activities(user, posts) do
|
||||
count_likes = Kernel.trunc(length(posts) / 4)
|
||||
IO.puts("Starting generating #{count_likes} like activities...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
Enum.take_random(posts, count_likes),
|
||||
fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting like activities take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
def generate_users(opts) do
|
||||
IO.puts("Starting generating #{opts[:users_max]} users...")
|
||||
{time, _} = :timer.tc(fn -> do_generate_users(opts) end)
|
||||
|
@ -31,7 +49,6 @@ defp generate_user_data(i) do
|
|||
password_hash:
|
||||
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
|
||||
bio: "Tester Number #{i}",
|
||||
info: %{},
|
||||
local: remote
|
||||
}
|
||||
|
||||
|
|
|
@ -100,6 +100,10 @@ def run(args) do
|
|||
|
||||
generate_remote_activities(user, remote_users)
|
||||
|
||||
generate_like_activities(
|
||||
user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create"))
|
||||
)
|
||||
|
||||
generate_dms(user, users, opts)
|
||||
|
||||
{:ok, activity} = generate_long_thread(user, users, opts)
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
config :pleroma, Pleroma.Upload,
|
||||
uploader: Pleroma.Uploaders.Local,
|
||||
filters: [Pleroma.Upload.Filter.Dedupe],
|
||||
link_name: true,
|
||||
link_name: false,
|
||||
proxy_remote: false,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
|
@ -180,7 +180,8 @@
|
|||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
level: :debug,
|
||||
format: "\n$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
config :logger, :ex_syslogger,
|
||||
|
@ -208,6 +209,7 @@
|
|||
config :pleroma, :http,
|
||||
proxy_url: nil,
|
||||
send_user_agent: true,
|
||||
user_agent: :default,
|
||||
adapter: [
|
||||
ssl_options: [
|
||||
# Workaround for remote server certificate chain issues
|
||||
|
@ -257,7 +259,7 @@
|
|||
mrf_transparency_exclusions: [],
|
||||
autofollowed_nicknames: [],
|
||||
max_pinned_statuses: 1,
|
||||
no_attachment_links: false,
|
||||
no_attachment_links: true,
|
||||
welcome_user_nickname: nil,
|
||||
welcome_message: nil,
|
||||
max_report_comment_size: 1000,
|
||||
|
@ -274,7 +276,13 @@
|
|||
account_field_name_length: 512,
|
||||
account_field_value_length: 2048,
|
||||
external_user_synchronization: true,
|
||||
extended_nickname_format: false
|
||||
extended_nickname_format: true
|
||||
|
||||
config :pleroma, :feed,
|
||||
post_title: %{
|
||||
max_length: 100,
|
||||
omission: "..."
|
||||
}
|
||||
|
||||
config :pleroma, :markup,
|
||||
# XXX - unfortunately, inline images must be enabled by default right now, because
|
||||
|
@ -375,6 +383,10 @@
|
|||
accept: [],
|
||||
reject: []
|
||||
|
||||
config :pleroma, :mrf_object_age,
|
||||
threshold: 172_800,
|
||||
actions: [:delist, :strip_followers]
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
enabled: true,
|
||||
ignore_hosts: [],
|
||||
|
@ -599,11 +611,13 @@
|
|||
|
||||
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
|
||||
|
||||
config :pleroma, :static_fe, enabled: false
|
||||
|
||||
config :pleroma, :web_cache_ttl,
|
||||
activity_pub: nil,
|
||||
activity_pub_question: 30_000
|
||||
|
||||
config :swarm, node_blacklist: [~r/myhtmlex_.*$/]
|
||||
config :swarm, node_blacklist: [~r/myhtml_.*$/]
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
config :phoenix, serve_endpoints: true
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :warn
|
||||
config :logger, :console, level: :warn
|
||||
config :logger, :ex_syslogger, level: :warn
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Config
|
||||
|
||||
config :pleroma, :instance, static: "/var/lib/pleroma/static"
|
||||
config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
|
||||
|
||||
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
method: Pleroma.Captcha.Mock
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
config :logger, :console,
|
||||
level: :warn,
|
||||
format: "\n[$level] $message\n"
|
||||
|
||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||
|
||||
|
|
|
@ -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
|
||||
- Response:
|
||||
- "ok"
|
||||
|
||||
## `/api/pleroma/admin/users/unfollow`
|
||||
## `POST /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
|
||||
- Response:
|
||||
- "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
|
||||
|
@ -239,30 +235,20 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
|
||||
|
||||
### Active or deactivate a user
|
||||
|
||||
- Params:
|
||||
- `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 +257,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 +283,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 +304,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/users/invites`
|
||||
## `GET /api/pleroma/admin/users/invites`
|
||||
|
||||
### Get a list of generated invites
|
||||
|
||||
- Methods: `GET`
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
|
@ -345,11 +329,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/users/revoke_invite`
|
||||
## `POST /api/pleroma/admin/users/revoke_invite`
|
||||
|
||||
### Revoke invite by token
|
||||
|
||||
- Methods: `POST`
|
||||
- Params:
|
||||
- `token`
|
||||
- Response:
|
||||
|
@ -367,21 +350,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 +372,18 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## `/api/pleroma/admin/users/:nickname/force_password_reset`
|
||||
## `PATCH /api/pleroma/admin/users/force_password_reset`
|
||||
|
||||
### Force passord reset for a user with a given nickname
|
||||
|
||||
- Methods: `PATCH`
|
||||
- Params: none
|
||||
- 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 +398,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
|
||||
```json
|
||||
{
|
||||
"total" : 1,
|
||||
"totalReports" : 1,
|
||||
"reports": [
|
||||
{
|
||||
"account": {
|
||||
|
@ -560,9 +540,34 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/reports/:id`
|
||||
## `GET /api/pleroma/admin/grouped_reports`
|
||||
|
||||
### Get a list of reports, grouped by status
|
||||
|
||||
- Params: none
|
||||
- On success: JSON, returns a list of reports, where:
|
||||
- `date`: date of the latest report
|
||||
- `account`: the user who has been reported (see `/api/pleroma/admin/reports` for reference)
|
||||
- `status`: reported status (see `/api/pleroma/admin/reports` for reference)
|
||||
- `actors`: users who had reported this status (see `/api/pleroma/admin/reports` for reference)
|
||||
- `reports`: reports (see `/api/pleroma/admin/reports` for reference)
|
||||
|
||||
```json
|
||||
"reports": [
|
||||
{
|
||||
"date": "2019-10-07T12:31:39.615149Z",
|
||||
"account": { ... },
|
||||
"status": { ... },
|
||||
"actors": [{ ... }, { ... }],
|
||||
"reports": [{ ... }]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## `GET /api/pleroma/admin/reports/:id`
|
||||
|
||||
### Get an individual report
|
||||
- Method `GET`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- Response:
|
||||
|
@ -571,22 +576,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 +680,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/statuses/:id`
|
||||
## `PUT /api/pleroma/admin/statuses/:id`
|
||||
|
||||
### Change the scope of an individual reported status
|
||||
- Method `PUT`
|
||||
|
||||
- Params:
|
||||
- `id`
|
||||
- `sensitive`: optional, valid values are `true` or `false`
|
||||
|
@ -670,9 +695,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
- 404 Not Found `"Not found"`
|
||||
- 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 +707,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 +720,12 @@ Copy settings on key `:pleroma` to DB.
|
|||
{}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/config/migrate_from_db`
|
||||
## `GET /api/pleroma/admin/config/migrate_from_db`
|
||||
|
||||
### Run mix task pleroma.config migrate_from_db
|
||||
|
||||
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
|
||||
- Method `GET`
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
|
@ -704,10 +733,12 @@ Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with dele
|
|||
{}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/config`
|
||||
## `GET /api/pleroma/admin/config`
|
||||
|
||||
### List config settings
|
||||
|
||||
List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
|
||||
- Method `GET`
|
||||
|
||||
- Params: none
|
||||
- Response:
|
||||
|
||||
|
@ -723,8 +754,10 @@ List config settings only works with `:pleroma => :instance => :dynamic_configur
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/config`
|
||||
## `POST /api/pleroma/admin/config`
|
||||
|
||||
### Update config settings
|
||||
|
||||
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 +780,6 @@ Compile time settings (need instance reboot):
|
|||
- `Pleroma.Upload` -> `:proxy_remote`
|
||||
- `:instance` -> `:upload_limit`
|
||||
|
||||
- Method `POST`
|
||||
- Params:
|
||||
- `configs` => [
|
||||
- `group` (string)
|
||||
|
@ -802,9 +834,10 @@ Compile time settings (need instance reboot):
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/moderation_log`
|
||||
## `GET /api/pleroma/admin/moderation_log`
|
||||
|
||||
### Get moderation log
|
||||
- Method `GET`
|
||||
|
||||
- Params:
|
||||
- *optional* `page`: **integer** page number
|
||||
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
|
||||
|
@ -831,8 +864,25 @@ 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
|
||||
|
||||
## `PATCH /api/pleroma/admin/users/confirm_email`
|
||||
|
||||
### Confirm users' emails
|
||||
|
||||
- Params:
|
||||
- `nicknames`
|
||||
- Response: Array of user nicknames
|
||||
|
||||
## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
|
||||
|
||||
### Resend confirmation email
|
||||
|
||||
- Params:
|
||||
- `nicknames`
|
||||
- Response: Array of user nicknames
|
||||
|
|
|
@ -57,6 +57,7 @@ Has these additional fields under the `pleroma` object:
|
|||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
|
||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
|
||||
- `deactivated`: boolean, true when the user is deactivated
|
||||
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
|
||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||
|
||||
### Source
|
||||
|
@ -72,6 +73,12 @@ Has an additional field under the `pleroma` object:
|
|||
|
||||
- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation.
|
||||
|
||||
## GET `/api/v1/conversations`
|
||||
|
||||
Accepts additional parameters:
|
||||
|
||||
- `recipients`: Only return conversations with the given recipients (a list of user ids). Usage example: `GET /api/v1/conversations?recipients[]=1&recipients[]=2`
|
||||
|
||||
## Account Search
|
||||
|
||||
Behavior has changed:
|
||||
|
@ -85,6 +92,12 @@ Has these additional fields under the `pleroma` object:
|
|||
|
||||
- `is_seen`: true if the notification was read by the user
|
||||
|
||||
### Move Notification
|
||||
|
||||
The `type` value is `move`. Has an additional field:
|
||||
|
||||
- `target`: new account
|
||||
|
||||
## GET `/api/v1/notifications`
|
||||
|
||||
Accepts additional parameters:
|
||||
|
@ -130,6 +143,7 @@ Additional parameters can be added to the JSON body/Form data:
|
|||
- `default_scope` - the scope returned under `privacy` key in Source subentity
|
||||
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
|
||||
- `skip_thread_containment` - if true, skip filtering out broken threads
|
||||
- `allow_following_move` - if true, allows automatically follow moved following accounts
|
||||
- `pleroma_background_image` - sets the background image of the user.
|
||||
|
||||
### Pleroma Settings Store
|
||||
|
|
|
@ -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..."}]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`.
|
||||
|
||||
!!! danger
|
||||
These mix tasks can take a long time to complete. Many of them were written to address specific database issues that happened because of bugs in migrations or other specific scenarios. Do not run these tasks "just in case" if everything is fine your instance.
|
||||
|
||||
## Replace embedded objects with their references
|
||||
|
||||
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
|
||||
|
|
|
@ -15,6 +15,11 @@ $PREFIX new <nickname> <email> [<options>]
|
|||
- `--admin`/`--no-admin` - whether the user should be an admin
|
||||
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
|
||||
|
||||
## List local users
|
||||
```sh
|
||||
$PREFIX list
|
||||
```
|
||||
|
||||
## Generate an invite link
|
||||
```sh
|
||||
$PREFIX invite [<options>]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
@ -12,7 +16,10 @@ The following packages need to be installed:
|
|||
* postgresql-contrib
|
||||
|
||||
To install them, run the following command (with doas or as root):
|
||||
`pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib`
|
||||
|
||||
```
|
||||
pkg_add elixir gmake ImageMagick git postgresql-server postgresql-contrib
|
||||
```
|
||||
|
||||
Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt.
|
||||
|
||||
|
@ -31,7 +38,7 @@ 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
|
||||
#### 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.
|
||||
|
||||
|
@ -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,7 +84,7 @@ 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.
|
||||
Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root):
|
||||
|
@ -107,7 +115,7 @@ domain <domain name> {
|
|||
challengedir "/var/www/acme/"
|
||||
}
|
||||
```
|
||||
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:
|
||||
|
@ -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
|
||||
|
@ -202,13 +210,15 @@ pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach par
|
|||
pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd
|
||||
pass in quick on $if proto tcp 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/`).
|
||||
|
||||
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.
|
||||
|
|
|
@ -42,6 +42,10 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
|
|||
## Setup
|
||||
### Configuring PostgreSQL
|
||||
#### (Optional) Installing RUM indexes
|
||||
|
||||
!!! warning
|
||||
It is recommended to use PostgreSQL v11 or newer. We have seen some minor issues with lower PostgreSQL versions.
|
||||
|
||||
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results).
|
||||
|
||||
Debian/Ubuntu (available only on Buster/19.04):
|
||||
|
|
|
@ -6,6 +6,11 @@ defmodule Mix.Pleroma do
|
|||
@doc "Common functions to be reused in mix tasks"
|
||||
def start_pleroma do
|
||||
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
|
||||
|
||||
if Pleroma.Config.get(:env) != :test do
|
||||
Application.put_env(:logger, :console, level: :debug)
|
||||
end
|
||||
|
||||
{:ok, _} = Application.ensure_all_started(:pleroma)
|
||||
end
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ def run(["migrate_from_db", env, delete?]) do
|
|||
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
|
||||
config_path = "config/#{env}.exported_from_db.secret.exs"
|
||||
|
||||
{:ok, file} = File.open(config_path, [:write])
|
||||
{:ok, file} = File.open(config_path, [:write, :utf8])
|
||||
IO.write(file, "use Mix.Config\r\n")
|
||||
|
||||
Repo.all(Config)
|
||||
|
|
|
@ -364,6 +364,24 @@ def run(["sign_out", nickname]) do
|
|||
end
|
||||
end
|
||||
|
||||
def run(["list"]) do
|
||||
start_pleroma()
|
||||
|
||||
Pleroma.User.Query.build(%{local: true})
|
||||
|> Pleroma.RepoStreamer.chunk_stream(500)
|
||||
|> Stream.each(fn users ->
|
||||
users
|
||||
|> Enum.each(fn user ->
|
||||
shell_info(
|
||||
"#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{
|
||||
user.info.locked
|
||||
}, deactivated: #{user.info.deactivated}"
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp set_moderator(user, value) do
|
||||
{:ok, user} =
|
||||
user
|
||||
|
|
|
@ -28,7 +28,8 @@ defmodule Pleroma.Activity do
|
|||
"Create" => "mention",
|
||||
"Follow" => "follow",
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite"
|
||||
"Like" => "favourite",
|
||||
"Move" => "move"
|
||||
}
|
||||
|
||||
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
|
||||
|
@ -41,6 +42,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 +91,19 @@ def with_preloaded_object(query, join_type \\ :inner) do
|
|||
|> preload([activity, object: object], object: object)
|
||||
end
|
||||
|
||||
def with_joined_user_actor(query, join_type \\ :inner) do
|
||||
join(query, join_type, [activity], u in User,
|
||||
on: u.ap_id == activity.actor,
|
||||
as: :user_actor
|
||||
)
|
||||
end
|
||||
|
||||
def with_preloaded_user_actor(query, join_type \\ :inner) do
|
||||
query
|
||||
|> with_joined_user_actor(join_type)
|
||||
|> preload([activity, user_actor: user_actor], user_actor: user_actor)
|
||||
end
|
||||
|
||||
def with_preloaded_bookmark(query, %User{} = user) do
|
||||
from([a] in query,
|
||||
left_join: b in Bookmark,
|
||||
|
@ -286,4 +304,17 @@ def restrict_deactivated_users(query) do
|
|||
end
|
||||
|
||||
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
|
||||
|
||||
def direct_conversation_id(activity, for_user) do
|
||||
alias Pleroma.Conversation.Participation
|
||||
|
||||
with %{data: %{"context" => context}} when is_binary(context) <- activity,
|
||||
%Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
|
||||
%Participation{id: participation_id} <-
|
||||
Participation.for_user_and_conversation(for_user, conversation) do
|
||||
participation_id
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,8 +17,14 @@ def named_version, do: @name <> " " <> @version
|
|||
def repository, do: @repository
|
||||
|
||||
def user_agent do
|
||||
case Pleroma.Config.get([:http, :user_agent], :default) do
|
||||
:default ->
|
||||
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
|
||||
named_version() <> "; " <> info
|
||||
|
||||
custom ->
|
||||
custom
|
||||
end
|
||||
end
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
|
@ -36,7 +42,8 @@ def start(_type, _args) do
|
|||
Pleroma.Emoji,
|
||||
Pleroma.Captcha,
|
||||
Pleroma.Daemons.ScheduledActivityDaemon,
|
||||
Pleroma.Daemons.ActivityExpirationDaemon
|
||||
Pleroma.Daemons.ActivityExpirationDaemon,
|
||||
Pleroma.Plugs.RateLimiter.Supervisor
|
||||
] ++
|
||||
cachex_children() ++
|
||||
hackney_pool_children() ++
|
||||
|
|
|
@ -9,6 +9,8 @@ defmodule Pleroma.Constants do
|
|||
|
||||
const(object_internal_fields,
|
||||
do: [
|
||||
"reactions",
|
||||
"reaction_count",
|
||||
"likes",
|
||||
"like_count",
|
||||
"announcements",
|
||||
|
|
|
@ -122,9 +122,37 @@ def for_user(user, params \\ %{}) do
|
|||
order_by: [desc: p.updated_at],
|
||||
preload: [conversation: [:users]]
|
||||
)
|
||||
|> restrict_recipients(user, params)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
def restrict_recipients(query, user, %{"recipients" => user_ids}) do
|
||||
user_ids =
|
||||
[user.id | user_ids]
|
||||
|> Enum.uniq()
|
||||
|> Enum.reduce([], fn user_id, acc ->
|
||||
case FlakeId.Ecto.CompatType.dump(user_id) do
|
||||
{:ok, user_id} -> [user_id | acc]
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
|
||||
conversation_subquery =
|
||||
__MODULE__
|
||||
|> group_by([p], p.conversation_id)
|
||||
|> having(
|
||||
[p],
|
||||
count(p.user_id) == ^length(user_ids) and
|
||||
fragment("array_agg(?) @> ?", p.user_id, ^user_ids)
|
||||
)
|
||||
|> select([p], %{id: p.conversation_id})
|
||||
|
||||
query
|
||||
|> join(:inner, [p], c in subquery(conversation_subquery), on: p.conversation_id == c.id)
|
||||
end
|
||||
|
||||
def restrict_recipients(query, _, _), do: query
|
||||
|
||||
def for_user_and_conversation(user, conversation) do
|
||||
from(p in __MODULE__,
|
||||
where: p.user_id == ^user.id,
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do
|
|||
def process(descriptions) do
|
||||
config_path = "docs/generate_config.json"
|
||||
|
||||
with {:ok, file} <- File.open(config_path, [:write]),
|
||||
with {:ok, file} <- File.open(config_path, [:write, :utf8]),
|
||||
json <- generate_json(descriptions),
|
||||
:ok <- IO.write(file, json),
|
||||
:ok <- File.close(file) do
|
||||
|
|
769
lib/pleroma/emoji-data.txt
Normal file
769
lib/pleroma/emoji-data.txt
Normal file
|
@ -0,0 +1,769 @@
|
|||
# emoji-data.txt
|
||||
# Date: 2019-01-15, 12:10:05 GMT
|
||||
# © 2019 Unicode®, Inc.
|
||||
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
|
||||
# For terms of use, see http://www.unicode.org/terms_of_use.html
|
||||
#
|
||||
# Emoji Data for UTS #51
|
||||
# Version: 12.0
|
||||
#
|
||||
# For documentation and usage, see http://www.unicode.org/reports/tr51
|
||||
#
|
||||
# Format:
|
||||
# <codepoint(s)> ; <property> # <comments>
|
||||
# Note: there is no guarantee as to the structure of whitespace or comments
|
||||
#
|
||||
# Characters and sequences are listed in code point order. Users should be shown a more natural order.
|
||||
# See the CLDR collation order for Emoji.
|
||||
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji=No
|
||||
# @missing: 0000..10FFFF ; Emoji ; No
|
||||
|
||||
0023 ; Emoji # 1.1 [1] (#️) number sign
|
||||
002A ; Emoji # 1.1 [1] (*️) asterisk
|
||||
0030..0039 ; Emoji # 1.1 [10] (0️..9️) digit zero..digit nine
|
||||
00A9 ; Emoji # 1.1 [1] (©️) copyright
|
||||
00AE ; Emoji # 1.1 [1] (®️) registered
|
||||
203C ; Emoji # 1.1 [1] (‼️) double exclamation mark
|
||||
2049 ; Emoji # 3.0 [1] (⁉️) exclamation question mark
|
||||
2122 ; Emoji # 1.1 [1] (™️) trade mark
|
||||
2139 ; Emoji # 3.0 [1] (ℹ️) information
|
||||
2194..2199 ; Emoji # 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
|
||||
21A9..21AA ; Emoji # 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
|
||||
231A..231B ; Emoji # 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
2328 ; Emoji # 1.1 [1] (⌨️) keyboard
|
||||
23CF ; Emoji # 4.0 [1] (⏏️) eject button
|
||||
23E9..23F3 ; Emoji # 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
|
||||
23F8..23FA ; Emoji # 7.0 [3] (⏸️..⏺️) pause button..record button
|
||||
24C2 ; Emoji # 1.1 [1] (Ⓜ️) circled M
|
||||
25AA..25AB ; Emoji # 1.1 [2] (▪️..▫️) black small square..white small square
|
||||
25B6 ; Emoji # 1.1 [1] (▶️) play button
|
||||
25C0 ; Emoji # 1.1 [1] (◀️) reverse button
|
||||
25FB..25FE ; Emoji # 3.2 [4] (◻️..◾) white medium square..black medium-small square
|
||||
2600..2604 ; Emoji # 1.1 [5] (☀️..☄️) sun..comet
|
||||
260E ; Emoji # 1.1 [1] (☎️) telephone
|
||||
2611 ; Emoji # 1.1 [1] (☑️) check box with check
|
||||
2614..2615 ; Emoji # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2618 ; Emoji # 4.1 [1] (☘️) shamrock
|
||||
261D ; Emoji # 1.1 [1] (☝️) index pointing up
|
||||
2620 ; Emoji # 1.1 [1] (☠️) skull and crossbones
|
||||
2622..2623 ; Emoji # 1.1 [2] (☢️..☣️) radioactive..biohazard
|
||||
2626 ; Emoji # 1.1 [1] (☦️) orthodox cross
|
||||
262A ; Emoji # 1.1 [1] (☪️) star and crescent
|
||||
262E..262F ; Emoji # 1.1 [2] (☮️..☯️) peace symbol..yin yang
|
||||
2638..263A ; Emoji # 1.1 [3] (☸️..☺️) wheel of dharma..smiling face
|
||||
2640 ; Emoji # 1.1 [1] (♀️) female sign
|
||||
2642 ; Emoji # 1.1 [1] (♂️) male sign
|
||||
2648..2653 ; Emoji # 1.1 [12] (♈..♓) Aries..Pisces
|
||||
265F..2660 ; Emoji # 1.1 [2] (♟️..♠️) chess pawn..spade suit
|
||||
2663 ; Emoji # 1.1 [1] (♣️) club suit
|
||||
2665..2666 ; Emoji # 1.1 [2] (♥️..♦️) heart suit..diamond suit
|
||||
2668 ; Emoji # 1.1 [1] (♨️) hot springs
|
||||
267B ; Emoji # 3.2 [1] (♻️) recycling symbol
|
||||
267E..267F ; Emoji # 4.1 [2] (♾️..♿) infinity..wheelchair symbol
|
||||
2692..2697 ; Emoji # 4.1 [6] (⚒️..⚗️) hammer and pick..alembic
|
||||
2699 ; Emoji # 4.1 [1] (⚙️) gear
|
||||
269B..269C ; Emoji # 4.1 [2] (⚛️..⚜️) atom symbol..fleur-de-lis
|
||||
26A0..26A1 ; Emoji # 4.0 [2] (⚠️..⚡) warning..high voltage
|
||||
26AA..26AB ; Emoji # 4.1 [2] (⚪..⚫) white circle..black circle
|
||||
26B0..26B1 ; Emoji # 4.1 [2] (⚰️..⚱️) coffin..funeral urn
|
||||
26BD..26BE ; Emoji # 5.2 [2] (⚽..⚾) soccer ball..baseball
|
||||
26C4..26C5 ; Emoji # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
|
||||
26C8 ; Emoji # 5.2 [1] (⛈️) cloud with lightning and rain
|
||||
26CE ; Emoji # 6.0 [1] (⛎) Ophiuchus
|
||||
26CF ; Emoji # 5.2 [1] (⛏️) pick
|
||||
26D1 ; Emoji # 5.2 [1] (⛑️) rescue worker’s helmet
|
||||
26D3..26D4 ; Emoji # 5.2 [2] (⛓️..⛔) chains..no entry
|
||||
26E9..26EA ; Emoji # 5.2 [2] (⛩️..⛪) shinto shrine..church
|
||||
26F0..26F5 ; Emoji # 5.2 [6] (⛰️..⛵) mountain..sailboat
|
||||
26F7..26FA ; Emoji # 5.2 [4] (⛷️..⛺) skier..tent
|
||||
26FD ; Emoji # 5.2 [1] (⛽) fuel pump
|
||||
2702 ; Emoji # 1.1 [1] (✂️) scissors
|
||||
2705 ; Emoji # 6.0 [1] (✅) check mark button
|
||||
2708..2709 ; Emoji # 1.1 [2] (✈️..✉️) airplane..envelope
|
||||
270A..270B ; Emoji # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..270D ; Emoji # 1.1 [2] (✌️..✍️) victory hand..writing hand
|
||||
270F ; Emoji # 1.1 [1] (✏️) pencil
|
||||
2712 ; Emoji # 1.1 [1] (✒️) black nib
|
||||
2714 ; Emoji # 1.1 [1] (✔️) check mark
|
||||
2716 ; Emoji # 1.1 [1] (✖️) multiplication sign
|
||||
271D ; Emoji # 1.1 [1] (✝️) latin cross
|
||||
2721 ; Emoji # 1.1 [1] (✡️) star of David
|
||||
2728 ; Emoji # 6.0 [1] (✨) sparkles
|
||||
2733..2734 ; Emoji # 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
|
||||
2744 ; Emoji # 1.1 [1] (❄️) snowflake
|
||||
2747 ; Emoji # 1.1 [1] (❇️) sparkle
|
||||
274C ; Emoji # 6.0 [1] (❌) cross mark
|
||||
274E ; Emoji # 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Emoji # 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Emoji # 5.2 [1] (❗) exclamation mark
|
||||
2763..2764 ; Emoji # 1.1 [2] (❣️..❤️) heart exclamation..red heart
|
||||
2795..2797 ; Emoji # 6.0 [3] (➕..➗) plus sign..division sign
|
||||
27A1 ; Emoji # 1.1 [1] (➡️) right arrow
|
||||
27B0 ; Emoji # 6.0 [1] (➰) curly loop
|
||||
27BF ; Emoji # 6.0 [1] (➿) double curly loop
|
||||
2934..2935 ; Emoji # 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
|
||||
2B05..2B07 ; Emoji # 4.0 [3] (⬅️..⬇️) left arrow..down arrow
|
||||
2B1B..2B1C ; Emoji # 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Emoji # 5.1 [1] (⭐) star
|
||||
2B55 ; Emoji # 5.2 [1] (⭕) hollow red circle
|
||||
3030 ; Emoji # 1.1 [1] (〰️) wavy dash
|
||||
303D ; Emoji # 3.2 [1] (〽️) part alternation mark
|
||||
3297 ; Emoji # 1.1 [1] (㊗️) Japanese “congratulations” button
|
||||
3299 ; Emoji # 1.1 [1] (㊙️) Japanese “secret” button
|
||||
1F004 ; Emoji # 5.1 [1] (🀄) mahjong red dragon
|
||||
1F0CF ; Emoji # 6.0 [1] (🃏) joker
|
||||
1F170..1F171 ; Emoji # 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
|
||||
1F17E ; Emoji # 6.0 [1] (🅾️) O button (blood type)
|
||||
1F17F ; Emoji # 5.2 [1] (🅿️) P button
|
||||
1F18E ; Emoji # 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Emoji # 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1E6..1F1FF ; Emoji # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F201..1F202 ; Emoji # 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
|
||||
1F21A ; Emoji # 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Emoji # 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F23A ; Emoji # 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
|
||||
1F250..1F251 ; Emoji # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F300..1F320 ; Emoji # 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F321 ; Emoji # 7.0 [1] (🌡️) thermometer
|
||||
1F324..1F32C ; Emoji # 7.0 [9] (🌤️..🌬️) sun behind small cloud..wind face
|
||||
1F32D..1F32F ; Emoji # 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Emoji # 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F336 ; Emoji # 7.0 [1] (🌶️) hot pepper
|
||||
1F337..1F37C ; Emoji # 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37D ; Emoji # 7.0 [1] (🍽️) fork and knife with plate
|
||||
1F37E..1F37F ; Emoji # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Emoji # 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F396..1F397 ; Emoji # 7.0 [2] (🎖️..🎗️) military medal..reminder ribbon
|
||||
1F399..1F39B ; Emoji # 7.0 [3] (🎙️..🎛️) studio microphone..control knobs
|
||||
1F39E..1F39F ; Emoji # 7.0 [2] (🎞️..🎟️) film frames..admission tickets
|
||||
1F3A0..1F3C4 ; Emoji # 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Emoji # 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Emoji # 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CB..1F3CE ; Emoji # 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
|
||||
1F3CF..1F3D3 ; Emoji # 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3D4..1F3DF ; Emoji # 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
|
||||
1F3E0..1F3F0 ; Emoji # 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F3..1F3F5 ; Emoji # 7.0 [3] (🏳️..🏵️) white flag..rosette
|
||||
1F3F7 ; Emoji # 7.0 [1] (🏷️) label
|
||||
1F3F8..1F3FF ; Emoji # 8.0 [8] (🏸..🏿) badminton..dark skin tone
|
||||
1F400..1F43E ; Emoji # 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F43F ; Emoji # 7.0 [1] (🐿️) chipmunk
|
||||
1F440 ; Emoji # 6.0 [1] (👀) eyes
|
||||
1F441 ; Emoji # 7.0 [1] (👁️) eye
|
||||
1F442..1F4F7 ; Emoji # 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Emoji # 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Emoji # 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FD ; Emoji # 7.0 [1] (📽️) film projector
|
||||
1F4FF ; Emoji # 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Emoji # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
|
||||
1F549..1F54A ; Emoji # 7.0 [2] (🕉️..🕊️) om..dove
|
||||
1F54B..1F54E ; Emoji # 8.0 [4] (🕋..🕎) kaaba..menorah
|
||||
1F550..1F567 ; Emoji # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F56F..1F570 ; Emoji # 7.0 [2] (🕯️..🕰️) candle..mantelpiece clock
|
||||
1F573..1F579 ; Emoji # 7.0 [7] (🕳️..🕹️) hole..joystick
|
||||
1F57A ; Emoji # 9.0 [1] (🕺) man dancing
|
||||
1F587 ; Emoji # 7.0 [1] (🖇️) linked paperclips
|
||||
1F58A..1F58D ; Emoji # 7.0 [4] (🖊️..🖍️) pen..crayon
|
||||
1F590 ; Emoji # 7.0 [1] (🖐️) hand with fingers splayed
|
||||
1F595..1F596 ; Emoji # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F5A4 ; Emoji # 9.0 [1] (🖤) black heart
|
||||
1F5A5 ; Emoji # 7.0 [1] (🖥️) desktop computer
|
||||
1F5A8 ; Emoji # 7.0 [1] (🖨️) printer
|
||||
1F5B1..1F5B2 ; Emoji # 7.0 [2] (🖱️..🖲️) computer mouse..trackball
|
||||
1F5BC ; Emoji # 7.0 [1] (🖼️) framed picture
|
||||
1F5C2..1F5C4 ; Emoji # 7.0 [3] (🗂️..🗄️) card index dividers..file cabinet
|
||||
1F5D1..1F5D3 ; Emoji # 7.0 [3] (🗑️..🗓️) wastebasket..spiral calendar
|
||||
1F5DC..1F5DE ; Emoji # 7.0 [3] (🗜️..🗞️) clamp..rolled-up newspaper
|
||||
1F5E1 ; Emoji # 7.0 [1] (🗡️) dagger
|
||||
1F5E3 ; Emoji # 7.0 [1] (🗣️) speaking head
|
||||
1F5E8 ; Emoji # 7.0 [1] (🗨️) left speech bubble
|
||||
1F5EF ; Emoji # 7.0 [1] (🗯️) right anger bubble
|
||||
1F5F3 ; Emoji # 7.0 [1] (🗳️) ballot box with ballot
|
||||
1F5FA ; Emoji # 7.0 [1] (🗺️) world map
|
||||
1F5FB..1F5FF ; Emoji # 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Emoji # 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Emoji # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Emoji # 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Emoji # 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Emoji # 6.1 [1] (😕) confused face
|
||||
1F616 ; Emoji # 6.0 [1] (😖) confounded face
|
||||
1F617 ; Emoji # 6.1 [1] (😗) kissing face
|
||||
1F618 ; Emoji # 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Emoji # 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Emoji # 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Emoji # 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Emoji # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Emoji # 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Emoji # 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Emoji # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Emoji # 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Emoji # 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Emoji # 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Emoji # 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Emoji # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Emoji # 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Emoji # 6.0 [12] (😵..🙀) dizzy face..weary cat
|
||||
1F641..1F642 ; Emoji # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Emoji # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Emoji # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Emoji # 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6CB..1F6CF ; Emoji # 7.0 [5] (🛋️..🛏️) couch and lamp..bed
|
||||
1F6D0 ; Emoji # 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Emoji # 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6D5 ; Emoji # 12.0 [1] (🛕) hindu temple
|
||||
1F6E0..1F6E5 ; Emoji # 7.0 [6] (🛠️..🛥️) hammer and wrench..motor boat
|
||||
1F6E9 ; Emoji # 7.0 [1] (🛩️) small airplane
|
||||
1F6EB..1F6EC ; Emoji # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
|
||||
1F6F0 ; Emoji # 7.0 [1] (🛰️) satellite
|
||||
1F6F3 ; Emoji # 7.0 [1] (🛳️) passenger ship
|
||||
1F6F4..1F6F6 ; Emoji # 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Emoji # 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F6F9 ; Emoji # 11.0 [1] (🛹) skateboard
|
||||
1F6FA ; Emoji # 12.0 [1] (🛺) auto rickshaw
|
||||
1F7E0..1F7EB ; Emoji # 12.0 [12] (🟠..🟫) orange circle..brown square
|
||||
1F90D..1F90F ; Emoji # 12.0 [3] (🤍..🤏) white heart..pinching hand
|
||||
1F910..1F918 ; Emoji # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Emoji # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Emoji # 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Emoji # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Emoji # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Emoji # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Emoji # 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Emoji # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F93F ; Emoji # 12.0 [1] (🤿) diving mask
|
||||
1F940..1F945 ; Emoji # 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Emoji # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Emoji # 10.0 [1] (🥌) curling stone
|
||||
1F94D..1F94F ; Emoji # 11.0 [3] (🥍..🥏) lacrosse..flying disc
|
||||
1F950..1F95E ; Emoji # 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Emoji # 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F96C..1F970 ; Emoji # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
|
||||
1F971 ; Emoji # 12.0 [1] (🥱) yawning face
|
||||
1F973..1F976 ; Emoji # 11.0 [4] (🥳..🥶) partying face..cold face
|
||||
1F97A ; Emoji # 11.0 [1] (🥺) pleading face
|
||||
1F97B ; Emoji # 12.0 [1] (🥻) sari
|
||||
1F97C..1F97F ; Emoji # 11.0 [4] (🥼..🥿) lab coat..flat shoe
|
||||
1F980..1F984 ; Emoji # 8.0 [5] (🦀..🦄) crab..unicorn
|
||||
1F985..1F991 ; Emoji # 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Emoji # 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F998..1F9A2 ; Emoji # 11.0 [11] (🦘..🦢) kangaroo..swan
|
||||
1F9A5..1F9AA ; Emoji # 12.0 [6] (🦥..🦪) sloth..oyster
|
||||
1F9AE..1F9AF ; Emoji # 12.0 [2] (🦮..🦯) guide dog..probing cane
|
||||
1F9B0..1F9B9 ; Emoji # 11.0 [10] (🦰..🦹) red hair..supervillain
|
||||
1F9BA..1F9BF ; Emoji # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
|
||||
1F9C0 ; Emoji # 8.0 [1] (🧀) cheese wedge
|
||||
1F9C1..1F9C2 ; Emoji # 11.0 [2] (🧁..🧂) cupcake..salt
|
||||
1F9C3..1F9CA ; Emoji # 12.0 [8] (🧃..🧊) beverage box..ice cube
|
||||
1F9CD..1F9CF ; Emoji # 12.0 [3] (🧍..🧏) person standing..deaf person
|
||||
1F9D0..1F9E6 ; Emoji # 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
1F9E7..1F9FF ; Emoji # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
|
||||
1FA70..1FA73 ; Emoji # 12.0 [4] (🩰..🩳) ballet shoes..shorts
|
||||
1FA78..1FA7A ; Emoji # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
|
||||
1FA80..1FA82 ; Emoji # 12.0 [3] (🪀..🪂) yo-yo..parachute
|
||||
1FA90..1FA95 ; Emoji # 12.0 [6] (🪐..🪕) ringed planet..banjo
|
||||
|
||||
# Total elements: 1311
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Presentation=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Presentation ; No
|
||||
|
||||
231A..231B ; Emoji_Presentation # 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
23E9..23EC ; Emoji_Presentation # 6.0 [4] (⏩..⏬) fast-forward button..fast down button
|
||||
23F0 ; Emoji_Presentation # 6.0 [1] (⏰) alarm clock
|
||||
23F3 ; Emoji_Presentation # 6.0 [1] (⏳) hourglass not done
|
||||
25FD..25FE ; Emoji_Presentation # 3.2 [2] (◽..◾) white medium-small square..black medium-small square
|
||||
2614..2615 ; Emoji_Presentation # 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2648..2653 ; Emoji_Presentation # 1.1 [12] (♈..♓) Aries..Pisces
|
||||
267F ; Emoji_Presentation # 4.1 [1] (♿) wheelchair symbol
|
||||
2693 ; Emoji_Presentation # 4.1 [1] (⚓) anchor
|
||||
26A1 ; Emoji_Presentation # 4.0 [1] (⚡) high voltage
|
||||
26AA..26AB ; Emoji_Presentation # 4.1 [2] (⚪..⚫) white circle..black circle
|
||||
26BD..26BE ; Emoji_Presentation # 5.2 [2] (⚽..⚾) soccer ball..baseball
|
||||
26C4..26C5 ; Emoji_Presentation # 5.2 [2] (⛄..⛅) snowman without snow..sun behind cloud
|
||||
26CE ; Emoji_Presentation # 6.0 [1] (⛎) Ophiuchus
|
||||
26D4 ; Emoji_Presentation # 5.2 [1] (⛔) no entry
|
||||
26EA ; Emoji_Presentation # 5.2 [1] (⛪) church
|
||||
26F2..26F3 ; Emoji_Presentation # 5.2 [2] (⛲..⛳) fountain..flag in hole
|
||||
26F5 ; Emoji_Presentation # 5.2 [1] (⛵) sailboat
|
||||
26FA ; Emoji_Presentation # 5.2 [1] (⛺) tent
|
||||
26FD ; Emoji_Presentation # 5.2 [1] (⛽) fuel pump
|
||||
2705 ; Emoji_Presentation # 6.0 [1] (✅) check mark button
|
||||
270A..270B ; Emoji_Presentation # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
2728 ; Emoji_Presentation # 6.0 [1] (✨) sparkles
|
||||
274C ; Emoji_Presentation # 6.0 [1] (❌) cross mark
|
||||
274E ; Emoji_Presentation # 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Emoji_Presentation # 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Emoji_Presentation # 5.2 [1] (❗) exclamation mark
|
||||
2795..2797 ; Emoji_Presentation # 6.0 [3] (➕..➗) plus sign..division sign
|
||||
27B0 ; Emoji_Presentation # 6.0 [1] (➰) curly loop
|
||||
27BF ; Emoji_Presentation # 6.0 [1] (➿) double curly loop
|
||||
2B1B..2B1C ; Emoji_Presentation # 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Emoji_Presentation # 5.1 [1] (⭐) star
|
||||
2B55 ; Emoji_Presentation # 5.2 [1] (⭕) hollow red circle
|
||||
1F004 ; Emoji_Presentation # 5.1 [1] (🀄) mahjong red dragon
|
||||
1F0CF ; Emoji_Presentation # 6.0 [1] (🃏) joker
|
||||
1F18E ; Emoji_Presentation # 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Emoji_Presentation # 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1E6..1F1FF ; Emoji_Presentation # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F201 ; Emoji_Presentation # 6.0 [1] (🈁) Japanese “here” button
|
||||
1F21A ; Emoji_Presentation # 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Emoji_Presentation # 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F236 ; Emoji_Presentation # 6.0 [5] (🈲..🈶) Japanese “prohibited” button..Japanese “not free of charge” button
|
||||
1F238..1F23A ; Emoji_Presentation # 6.0 [3] (🈸..🈺) Japanese “application” button..Japanese “open for business” button
|
||||
1F250..1F251 ; Emoji_Presentation # 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F300..1F320 ; Emoji_Presentation # 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F32D..1F32F ; Emoji_Presentation # 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Emoji_Presentation # 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F337..1F37C ; Emoji_Presentation # 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37E..1F37F ; Emoji_Presentation # 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Emoji_Presentation # 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F3A0..1F3C4 ; Emoji_Presentation # 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Emoji_Presentation # 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Emoji_Presentation # 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CF..1F3D3 ; Emoji_Presentation # 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3E0..1F3F0 ; Emoji_Presentation # 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F4 ; Emoji_Presentation # 7.0 [1] (🏴) black flag
|
||||
1F3F8..1F3FF ; Emoji_Presentation # 8.0 [8] (🏸..🏿) badminton..dark skin tone
|
||||
1F400..1F43E ; Emoji_Presentation # 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F440 ; Emoji_Presentation # 6.0 [1] (👀) eyes
|
||||
1F442..1F4F7 ; Emoji_Presentation # 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Emoji_Presentation # 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Emoji_Presentation # 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FF ; Emoji_Presentation # 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Emoji_Presentation # 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
|
||||
1F54B..1F54E ; Emoji_Presentation # 8.0 [4] (🕋..🕎) kaaba..menorah
|
||||
1F550..1F567 ; Emoji_Presentation # 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F57A ; Emoji_Presentation # 9.0 [1] (🕺) man dancing
|
||||
1F595..1F596 ; Emoji_Presentation # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F5A4 ; Emoji_Presentation # 9.0 [1] (🖤) black heart
|
||||
1F5FB..1F5FF ; Emoji_Presentation # 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Emoji_Presentation # 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Emoji_Presentation # 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Emoji_Presentation # 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Emoji_Presentation # 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Emoji_Presentation # 6.1 [1] (😕) confused face
|
||||
1F616 ; Emoji_Presentation # 6.0 [1] (😖) confounded face
|
||||
1F617 ; Emoji_Presentation # 6.1 [1] (😗) kissing face
|
||||
1F618 ; Emoji_Presentation # 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Emoji_Presentation # 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Emoji_Presentation # 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Emoji_Presentation # 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Emoji_Presentation # 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Emoji_Presentation # 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Emoji_Presentation # 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Emoji_Presentation # 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Emoji_Presentation # 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Emoji_Presentation # 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Emoji_Presentation # 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Emoji_Presentation # 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Emoji_Presentation # 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Emoji_Presentation # 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Emoji_Presentation # 6.0 [12] (😵..🙀) dizzy face..weary cat
|
||||
1F641..1F642 ; Emoji_Presentation # 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Emoji_Presentation # 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Emoji_Presentation # 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Emoji_Presentation # 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6CC ; Emoji_Presentation # 7.0 [1] (🛌) person in bed
|
||||
1F6D0 ; Emoji_Presentation # 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Emoji_Presentation # 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6D5 ; Emoji_Presentation # 12.0 [1] (🛕) hindu temple
|
||||
1F6EB..1F6EC ; Emoji_Presentation # 7.0 [2] (🛫..🛬) airplane departure..airplane arrival
|
||||
1F6F4..1F6F6 ; Emoji_Presentation # 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Emoji_Presentation # 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F6F9 ; Emoji_Presentation # 11.0 [1] (🛹) skateboard
|
||||
1F6FA ; Emoji_Presentation # 12.0 [1] (🛺) auto rickshaw
|
||||
1F7E0..1F7EB ; Emoji_Presentation # 12.0 [12] (🟠..🟫) orange circle..brown square
|
||||
1F90D..1F90F ; Emoji_Presentation # 12.0 [3] (🤍..🤏) white heart..pinching hand
|
||||
1F910..1F918 ; Emoji_Presentation # 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Emoji_Presentation # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Emoji_Presentation # 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Emoji_Presentation # 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Emoji_Presentation # 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Emoji_Presentation # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji_Presentation # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Emoji_Presentation # 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Emoji_Presentation # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F93F ; Emoji_Presentation # 12.0 [1] (🤿) diving mask
|
||||
1F940..1F945 ; Emoji_Presentation # 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Emoji_Presentation # 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Emoji_Presentation # 10.0 [1] (🥌) curling stone
|
||||
1F94D..1F94F ; Emoji_Presentation # 11.0 [3] (🥍..🥏) lacrosse..flying disc
|
||||
1F950..1F95E ; Emoji_Presentation # 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Emoji_Presentation # 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F96C..1F970 ; Emoji_Presentation # 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
|
||||
1F971 ; Emoji_Presentation # 12.0 [1] (🥱) yawning face
|
||||
1F973..1F976 ; Emoji_Presentation # 11.0 [4] (🥳..🥶) partying face..cold face
|
||||
1F97A ; Emoji_Presentation # 11.0 [1] (🥺) pleading face
|
||||
1F97B ; Emoji_Presentation # 12.0 [1] (🥻) sari
|
||||
1F97C..1F97F ; Emoji_Presentation # 11.0 [4] (🥼..🥿) lab coat..flat shoe
|
||||
1F980..1F984 ; Emoji_Presentation # 8.0 [5] (🦀..🦄) crab..unicorn
|
||||
1F985..1F991 ; Emoji_Presentation # 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Emoji_Presentation # 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F998..1F9A2 ; Emoji_Presentation # 11.0 [11] (🦘..🦢) kangaroo..swan
|
||||
1F9A5..1F9AA ; Emoji_Presentation # 12.0 [6] (🦥..🦪) sloth..oyster
|
||||
1F9AE..1F9AF ; Emoji_Presentation # 12.0 [2] (🦮..🦯) guide dog..probing cane
|
||||
1F9B0..1F9B9 ; Emoji_Presentation # 11.0 [10] (🦰..🦹) red hair..supervillain
|
||||
1F9BA..1F9BF ; Emoji_Presentation # 12.0 [6] (🦺..🦿) safety vest..mechanical leg
|
||||
1F9C0 ; Emoji_Presentation # 8.0 [1] (🧀) cheese wedge
|
||||
1F9C1..1F9C2 ; Emoji_Presentation # 11.0 [2] (🧁..🧂) cupcake..salt
|
||||
1F9C3..1F9CA ; Emoji_Presentation # 12.0 [8] (🧃..🧊) beverage box..ice cube
|
||||
1F9CD..1F9CF ; Emoji_Presentation # 12.0 [3] (🧍..🧏) person standing..deaf person
|
||||
1F9D0..1F9E6 ; Emoji_Presentation # 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
1F9E7..1F9FF ; Emoji_Presentation # 11.0 [25] (🧧..🧿) red envelope..nazar amulet
|
||||
1FA70..1FA73 ; Emoji_Presentation # 12.0 [4] (🩰..🩳) ballet shoes..shorts
|
||||
1FA78..1FA7A ; Emoji_Presentation # 12.0 [3] (🩸..🩺) drop of blood..stethoscope
|
||||
1FA80..1FA82 ; Emoji_Presentation # 12.0 [3] (🪀..🪂) yo-yo..parachute
|
||||
1FA90..1FA95 ; Emoji_Presentation # 12.0 [6] (🪐..🪕) ringed planet..banjo
|
||||
|
||||
# Total elements: 1093
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Modifier=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Modifier ; No
|
||||
|
||||
1F3FB..1F3FF ; Emoji_Modifier # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
|
||||
|
||||
# Total elements: 5
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Modifier_Base=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Modifier_Base ; No
|
||||
|
||||
261D ; Emoji_Modifier_Base # 1.1 [1] (☝️) index pointing up
|
||||
26F9 ; Emoji_Modifier_Base # 5.2 [1] (⛹️) person bouncing ball
|
||||
270A..270B ; Emoji_Modifier_Base # 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..270D ; Emoji_Modifier_Base # 1.1 [2] (✌️..✍️) victory hand..writing hand
|
||||
1F385 ; Emoji_Modifier_Base # 6.0 [1] (🎅) Santa Claus
|
||||
1F3C2..1F3C4 ; Emoji_Modifier_Base # 6.0 [3] (🏂..🏄) snowboarder..person surfing
|
||||
1F3C7 ; Emoji_Modifier_Base # 6.0 [1] (🏇) horse racing
|
||||
1F3CA ; Emoji_Modifier_Base # 6.0 [1] (🏊) person swimming
|
||||
1F3CB..1F3CC ; Emoji_Modifier_Base # 7.0 [2] (🏋️..🏌️) person lifting weights..person golfing
|
||||
1F442..1F443 ; Emoji_Modifier_Base # 6.0 [2] (👂..👃) ear..nose
|
||||
1F446..1F450 ; Emoji_Modifier_Base # 6.0 [11] (👆..👐) backhand index pointing up..open hands
|
||||
1F466..1F478 ; Emoji_Modifier_Base # 6.0 [19] (👦..👸) boy..princess
|
||||
1F47C ; Emoji_Modifier_Base # 6.0 [1] (👼) baby angel
|
||||
1F481..1F483 ; Emoji_Modifier_Base # 6.0 [3] (💁..💃) person tipping hand..woman dancing
|
||||
1F485..1F487 ; Emoji_Modifier_Base # 6.0 [3] (💅..💇) nail polish..person getting haircut
|
||||
1F48F ; Emoji_Modifier_Base # 6.0 [1] (💏) kiss
|
||||
1F491 ; Emoji_Modifier_Base # 6.0 [1] (💑) couple with heart
|
||||
1F4AA ; Emoji_Modifier_Base # 6.0 [1] (💪) flexed biceps
|
||||
1F574..1F575 ; Emoji_Modifier_Base # 7.0 [2] (🕴️..🕵️) man in suit levitating..detective
|
||||
1F57A ; Emoji_Modifier_Base # 9.0 [1] (🕺) man dancing
|
||||
1F590 ; Emoji_Modifier_Base # 7.0 [1] (🖐️) hand with fingers splayed
|
||||
1F595..1F596 ; Emoji_Modifier_Base # 7.0 [2] (🖕..🖖) middle finger..vulcan salute
|
||||
1F645..1F647 ; Emoji_Modifier_Base # 6.0 [3] (🙅..🙇) person gesturing NO..person bowing
|
||||
1F64B..1F64F ; Emoji_Modifier_Base # 6.0 [5] (🙋..🙏) person raising hand..folded hands
|
||||
1F6A3 ; Emoji_Modifier_Base # 6.0 [1] (🚣) person rowing boat
|
||||
1F6B4..1F6B6 ; Emoji_Modifier_Base # 6.0 [3] (🚴..🚶) person biking..person walking
|
||||
1F6C0 ; Emoji_Modifier_Base # 6.0 [1] (🛀) person taking bath
|
||||
1F6CC ; Emoji_Modifier_Base # 7.0 [1] (🛌) person in bed
|
||||
1F90F ; Emoji_Modifier_Base # 12.0 [1] (🤏) pinching hand
|
||||
1F918 ; Emoji_Modifier_Base # 8.0 [1] (🤘) sign of the horns
|
||||
1F919..1F91E ; Emoji_Modifier_Base # 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Emoji_Modifier_Base # 10.0 [1] (🤟) love-you gesture
|
||||
1F926 ; Emoji_Modifier_Base # 9.0 [1] (🤦) person facepalming
|
||||
1F930 ; Emoji_Modifier_Base # 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Emoji_Modifier_Base # 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F939 ; Emoji_Modifier_Base # 9.0 [7] (🤳..🤹) selfie..person juggling
|
||||
1F93C..1F93E ; Emoji_Modifier_Base # 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F9B5..1F9B6 ; Emoji_Modifier_Base # 11.0 [2] (🦵..🦶) leg..foot
|
||||
1F9B8..1F9B9 ; Emoji_Modifier_Base # 11.0 [2] (🦸..🦹) superhero..supervillain
|
||||
1F9BB ; Emoji_Modifier_Base # 12.0 [1] (🦻) ear with hearing aid
|
||||
1F9CD..1F9CF ; Emoji_Modifier_Base # 12.0 [3] (🧍..🧏) person standing..deaf person
|
||||
1F9D1..1F9DD ; Emoji_Modifier_Base # 10.0 [13] (🧑..🧝) person..elf
|
||||
|
||||
# Total elements: 120
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Emoji_Component=No
|
||||
# @missing: 0000..10FFFF ; Emoji_Component ; No
|
||||
|
||||
0023 ; Emoji_Component # 1.1 [1] (#️) number sign
|
||||
002A ; Emoji_Component # 1.1 [1] (*️) asterisk
|
||||
0030..0039 ; Emoji_Component # 1.1 [10] (0️..9️) digit zero..digit nine
|
||||
200D ; Emoji_Component # 1.1 [1] () zero width joiner
|
||||
20E3 ; Emoji_Component # 3.0 [1] (⃣) combining enclosing keycap
|
||||
FE0F ; Emoji_Component # 3.2 [1] () VARIATION SELECTOR-16
|
||||
1F1E6..1F1FF ; Emoji_Component # 6.0 [26] (🇦..🇿) regional indicator symbol letter a..regional indicator symbol letter z
|
||||
1F3FB..1F3FF ; Emoji_Component # 8.0 [5] (🏻..🏿) light skin tone..dark skin tone
|
||||
1F9B0..1F9B3 ; Emoji_Component # 11.0 [4] (🦰..🦳) red hair..white hair
|
||||
E0020..E007F ; Emoji_Component # 3.1 [96] (..) tag space..cancel tag
|
||||
|
||||
# Total elements: 146
|
||||
|
||||
# ================================================
|
||||
|
||||
# All omitted code points have Extended_Pictographic=No
|
||||
# @missing: 0000..10FFFF ; Extended_Pictographic ; No
|
||||
|
||||
00A9 ; Extended_Pictographic# 1.1 [1] (©️) copyright
|
||||
00AE ; Extended_Pictographic# 1.1 [1] (®️) registered
|
||||
203C ; Extended_Pictographic# 1.1 [1] (‼️) double exclamation mark
|
||||
2049 ; Extended_Pictographic# 3.0 [1] (⁉️) exclamation question mark
|
||||
2122 ; Extended_Pictographic# 1.1 [1] (™️) trade mark
|
||||
2139 ; Extended_Pictographic# 3.0 [1] (ℹ️) information
|
||||
2194..2199 ; Extended_Pictographic# 1.1 [6] (↔️..↙️) left-right arrow..down-left arrow
|
||||
21A9..21AA ; Extended_Pictographic# 1.1 [2] (↩️..↪️) right arrow curving left..left arrow curving right
|
||||
231A..231B ; Extended_Pictographic# 1.1 [2] (⌚..⌛) watch..hourglass done
|
||||
2328 ; Extended_Pictographic# 1.1 [1] (⌨️) keyboard
|
||||
2388 ; Extended_Pictographic# 3.0 [1] (⎈) HELM SYMBOL
|
||||
23CF ; Extended_Pictographic# 4.0 [1] (⏏️) eject button
|
||||
23E9..23F3 ; Extended_Pictographic# 6.0 [11] (⏩..⏳) fast-forward button..hourglass not done
|
||||
23F8..23FA ; Extended_Pictographic# 7.0 [3] (⏸️..⏺️) pause button..record button
|
||||
24C2 ; Extended_Pictographic# 1.1 [1] (Ⓜ️) circled M
|
||||
25AA..25AB ; Extended_Pictographic# 1.1 [2] (▪️..▫️) black small square..white small square
|
||||
25B6 ; Extended_Pictographic# 1.1 [1] (▶️) play button
|
||||
25C0 ; Extended_Pictographic# 1.1 [1] (◀️) reverse button
|
||||
25FB..25FE ; Extended_Pictographic# 3.2 [4] (◻️..◾) white medium square..black medium-small square
|
||||
2600..2605 ; Extended_Pictographic# 1.1 [6] (☀️..★) sun..BLACK STAR
|
||||
2607..2612 ; Extended_Pictographic# 1.1 [12] (☇..☒) LIGHTNING..BALLOT BOX WITH X
|
||||
2614..2615 ; Extended_Pictographic# 4.0 [2] (☔..☕) umbrella with rain drops..hot beverage
|
||||
2616..2617 ; Extended_Pictographic# 3.2 [2] (☖..☗) WHITE SHOGI PIECE..BLACK SHOGI PIECE
|
||||
2618 ; Extended_Pictographic# 4.1 [1] (☘️) shamrock
|
||||
2619 ; Extended_Pictographic# 3.0 [1] (☙) REVERSED ROTATED FLORAL HEART BULLET
|
||||
261A..266F ; Extended_Pictographic# 1.1 [86] (☚..♯) BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN
|
||||
2670..2671 ; Extended_Pictographic# 3.0 [2] (♰..♱) WEST SYRIAC CROSS..EAST SYRIAC CROSS
|
||||
2672..267D ; Extended_Pictographic# 3.2 [12] (♲..♽) UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL
|
||||
267E..267F ; Extended_Pictographic# 4.1 [2] (♾️..♿) infinity..wheelchair symbol
|
||||
2680..2685 ; Extended_Pictographic# 3.2 [6] (⚀..⚅) DIE FACE-1..DIE FACE-6
|
||||
2690..2691 ; Extended_Pictographic# 4.0 [2] (⚐..⚑) WHITE FLAG..BLACK FLAG
|
||||
2692..269C ; Extended_Pictographic# 4.1 [11] (⚒️..⚜️) hammer and pick..fleur-de-lis
|
||||
269D ; Extended_Pictographic# 5.1 [1] (⚝) OUTLINED WHITE STAR
|
||||
269E..269F ; Extended_Pictographic# 5.2 [2] (⚞..⚟) THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT
|
||||
26A0..26A1 ; Extended_Pictographic# 4.0 [2] (⚠️..⚡) warning..high voltage
|
||||
26A2..26B1 ; Extended_Pictographic# 4.1 [16] (⚢..⚱️) DOUBLED FEMALE SIGN..funeral urn
|
||||
26B2 ; Extended_Pictographic# 5.0 [1] (⚲) NEUTER
|
||||
26B3..26BC ; Extended_Pictographic# 5.1 [10] (⚳..⚼) CERES..SESQUIQUADRATE
|
||||
26BD..26BF ; Extended_Pictographic# 5.2 [3] (⚽..⚿) soccer ball..SQUARED KEY
|
||||
26C0..26C3 ; Extended_Pictographic# 5.1 [4] (⛀..⛃) WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING
|
||||
26C4..26CD ; Extended_Pictographic# 5.2 [10] (⛄..⛍) snowman without snow..DISABLED CAR
|
||||
26CE ; Extended_Pictographic# 6.0 [1] (⛎) Ophiuchus
|
||||
26CF..26E1 ; Extended_Pictographic# 5.2 [19] (⛏️..⛡) pick..RESTRICTED LEFT ENTRY-2
|
||||
26E2 ; Extended_Pictographic# 6.0 [1] (⛢) ASTRONOMICAL SYMBOL FOR URANUS
|
||||
26E3 ; Extended_Pictographic# 5.2 [1] (⛣) HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE
|
||||
26E4..26E7 ; Extended_Pictographic# 6.0 [4] (⛤..⛧) PENTAGRAM..INVERTED PENTAGRAM
|
||||
26E8..26FF ; Extended_Pictographic# 5.2 [24] (⛨..⛿) BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE
|
||||
2700 ; Extended_Pictographic# 7.0 [1] (✀) BLACK SAFETY SCISSORS
|
||||
2701..2704 ; Extended_Pictographic# 1.1 [4] (✁..✄) UPPER BLADE SCISSORS..WHITE SCISSORS
|
||||
2705 ; Extended_Pictographic# 6.0 [1] (✅) check mark button
|
||||
2708..2709 ; Extended_Pictographic# 1.1 [2] (✈️..✉️) airplane..envelope
|
||||
270A..270B ; Extended_Pictographic# 6.0 [2] (✊..✋) raised fist..raised hand
|
||||
270C..2712 ; Extended_Pictographic# 1.1 [7] (✌️..✒️) victory hand..black nib
|
||||
2714 ; Extended_Pictographic# 1.1 [1] (✔️) check mark
|
||||
2716 ; Extended_Pictographic# 1.1 [1] (✖️) multiplication sign
|
||||
271D ; Extended_Pictographic# 1.1 [1] (✝️) latin cross
|
||||
2721 ; Extended_Pictographic# 1.1 [1] (✡️) star of David
|
||||
2728 ; Extended_Pictographic# 6.0 [1] (✨) sparkles
|
||||
2733..2734 ; Extended_Pictographic# 1.1 [2] (✳️..✴️) eight-spoked asterisk..eight-pointed star
|
||||
2744 ; Extended_Pictographic# 1.1 [1] (❄️) snowflake
|
||||
2747 ; Extended_Pictographic# 1.1 [1] (❇️) sparkle
|
||||
274C ; Extended_Pictographic# 6.0 [1] (❌) cross mark
|
||||
274E ; Extended_Pictographic# 6.0 [1] (❎) cross mark button
|
||||
2753..2755 ; Extended_Pictographic# 6.0 [3] (❓..❕) question mark..white exclamation mark
|
||||
2757 ; Extended_Pictographic# 5.2 [1] (❗) exclamation mark
|
||||
2763..2767 ; Extended_Pictographic# 1.1 [5] (❣️..❧) heart exclamation..ROTATED FLORAL HEART BULLET
|
||||
2795..2797 ; Extended_Pictographic# 6.0 [3] (➕..➗) plus sign..division sign
|
||||
27A1 ; Extended_Pictographic# 1.1 [1] (➡️) right arrow
|
||||
27B0 ; Extended_Pictographic# 6.0 [1] (➰) curly loop
|
||||
27BF ; Extended_Pictographic# 6.0 [1] (➿) double curly loop
|
||||
2934..2935 ; Extended_Pictographic# 3.2 [2] (⤴️..⤵️) right arrow curving up..right arrow curving down
|
||||
2B05..2B07 ; Extended_Pictographic# 4.0 [3] (⬅️..⬇️) left arrow..down arrow
|
||||
2B1B..2B1C ; Extended_Pictographic# 5.1 [2] (⬛..⬜) black large square..white large square
|
||||
2B50 ; Extended_Pictographic# 5.1 [1] (⭐) star
|
||||
2B55 ; Extended_Pictographic# 5.2 [1] (⭕) hollow red circle
|
||||
3030 ; Extended_Pictographic# 1.1 [1] (〰️) wavy dash
|
||||
303D ; Extended_Pictographic# 3.2 [1] (〽️) part alternation mark
|
||||
3297 ; Extended_Pictographic# 1.1 [1] (㊗️) Japanese “congratulations” button
|
||||
3299 ; Extended_Pictographic# 1.1 [1] (㊙️) Japanese “secret” button
|
||||
1F000..1F02B ; Extended_Pictographic# 5.1 [44] (🀀..🀫) MAHJONG TILE EAST WIND..MAHJONG TILE BACK
|
||||
1F02C..1F02F ; Extended_Pictographic# NA [4] (..) <reserved-1F02C>..<reserved-1F02F>
|
||||
1F030..1F093 ; Extended_Pictographic# 5.1[100] (🀰..🂓) DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06
|
||||
1F094..1F09F ; Extended_Pictographic# NA [12] (..) <reserved-1F094>..<reserved-1F09F>
|
||||
1F0A0..1F0AE ; Extended_Pictographic# 6.0 [15] (🂠..🂮) PLAYING CARD BACK..PLAYING CARD KING OF SPADES
|
||||
1F0AF..1F0B0 ; Extended_Pictographic# NA [2] (..) <reserved-1F0AF>..<reserved-1F0B0>
|
||||
1F0B1..1F0BE ; Extended_Pictographic# 6.0 [14] (🂱..🂾) PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS
|
||||
1F0BF ; Extended_Pictographic# 7.0 [1] (🂿) PLAYING CARD RED JOKER
|
||||
1F0C0 ; Extended_Pictographic# NA [1] () <reserved-1F0C0>
|
||||
1F0C1..1F0CF ; Extended_Pictographic# 6.0 [15] (🃁..🃏) PLAYING CARD ACE OF DIAMONDS..joker
|
||||
1F0D0 ; Extended_Pictographic# NA [1] () <reserved-1F0D0>
|
||||
1F0D1..1F0DF ; Extended_Pictographic# 6.0 [15] (🃑..🃟) PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER
|
||||
1F0E0..1F0F5 ; Extended_Pictographic# 7.0 [22] (🃠..🃵) PLAYING CARD FOOL..PLAYING CARD TRUMP-21
|
||||
1F0F6..1F0FF ; Extended_Pictographic# NA [10] (..) <reserved-1F0F6>..<reserved-1F0FF>
|
||||
1F10D..1F10F ; Extended_Pictographic# NA [3] (🄍..🄏) <reserved-1F10D>..<reserved-1F10F>
|
||||
1F12F ; Extended_Pictographic# 11.0 [1] (🄯) COPYLEFT SYMBOL
|
||||
1F16C ; Extended_Pictographic# 12.0 [1] (🅬) RAISED MR SIGN
|
||||
1F16D..1F16F ; Extended_Pictographic# NA [3] (🅭..🅯) <reserved-1F16D>..<reserved-1F16F>
|
||||
1F170..1F171 ; Extended_Pictographic# 6.0 [2] (🅰️..🅱️) A button (blood type)..B button (blood type)
|
||||
1F17E ; Extended_Pictographic# 6.0 [1] (🅾️) O button (blood type)
|
||||
1F17F ; Extended_Pictographic# 5.2 [1] (🅿️) P button
|
||||
1F18E ; Extended_Pictographic# 6.0 [1] (🆎) AB button (blood type)
|
||||
1F191..1F19A ; Extended_Pictographic# 6.0 [10] (🆑..🆚) CL button..VS button
|
||||
1F1AD..1F1E5 ; Extended_Pictographic# NA [57] (🆭..) <reserved-1F1AD>..<reserved-1F1E5>
|
||||
1F201..1F202 ; Extended_Pictographic# 6.0 [2] (🈁..🈂️) Japanese “here” button..Japanese “service charge” button
|
||||
1F203..1F20F ; Extended_Pictographic# NA [13] (..) <reserved-1F203>..<reserved-1F20F>
|
||||
1F21A ; Extended_Pictographic# 5.2 [1] (🈚) Japanese “free of charge” button
|
||||
1F22F ; Extended_Pictographic# 5.2 [1] (🈯) Japanese “reserved” button
|
||||
1F232..1F23A ; Extended_Pictographic# 6.0 [9] (🈲..🈺) Japanese “prohibited” button..Japanese “open for business” button
|
||||
1F23C..1F23F ; Extended_Pictographic# NA [4] (..) <reserved-1F23C>..<reserved-1F23F>
|
||||
1F249..1F24F ; Extended_Pictographic# NA [7] (..) <reserved-1F249>..<reserved-1F24F>
|
||||
1F250..1F251 ; Extended_Pictographic# 6.0 [2] (🉐..🉑) Japanese “bargain” button..Japanese “acceptable” button
|
||||
1F252..1F25F ; Extended_Pictographic# NA [14] (..) <reserved-1F252>..<reserved-1F25F>
|
||||
1F260..1F265 ; Extended_Pictographic# 10.0 [6] (🉠..🉥) ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI
|
||||
1F266..1F2FF ; Extended_Pictographic# NA[154] (..) <reserved-1F266>..<reserved-1F2FF>
|
||||
1F300..1F320 ; Extended_Pictographic# 6.0 [33] (🌀..🌠) cyclone..shooting star
|
||||
1F321..1F32C ; Extended_Pictographic# 7.0 [12] (🌡️..🌬️) thermometer..wind face
|
||||
1F32D..1F32F ; Extended_Pictographic# 8.0 [3] (🌭..🌯) hot dog..burrito
|
||||
1F330..1F335 ; Extended_Pictographic# 6.0 [6] (🌰..🌵) chestnut..cactus
|
||||
1F336 ; Extended_Pictographic# 7.0 [1] (🌶️) hot pepper
|
||||
1F337..1F37C ; Extended_Pictographic# 6.0 [70] (🌷..🍼) tulip..baby bottle
|
||||
1F37D ; Extended_Pictographic# 7.0 [1] (🍽️) fork and knife with plate
|
||||
1F37E..1F37F ; Extended_Pictographic# 8.0 [2] (🍾..🍿) bottle with popping cork..popcorn
|
||||
1F380..1F393 ; Extended_Pictographic# 6.0 [20] (🎀..🎓) ribbon..graduation cap
|
||||
1F394..1F39F ; Extended_Pictographic# 7.0 [12] (🎔..🎟️) HEART WITH TIP ON THE LEFT..admission tickets
|
||||
1F3A0..1F3C4 ; Extended_Pictographic# 6.0 [37] (🎠..🏄) carousel horse..person surfing
|
||||
1F3C5 ; Extended_Pictographic# 7.0 [1] (🏅) sports medal
|
||||
1F3C6..1F3CA ; Extended_Pictographic# 6.0 [5] (🏆..🏊) trophy..person swimming
|
||||
1F3CB..1F3CE ; Extended_Pictographic# 7.0 [4] (🏋️..🏎️) person lifting weights..racing car
|
||||
1F3CF..1F3D3 ; Extended_Pictographic# 8.0 [5] (🏏..🏓) cricket game..ping pong
|
||||
1F3D4..1F3DF ; Extended_Pictographic# 7.0 [12] (🏔️..🏟️) snow-capped mountain..stadium
|
||||
1F3E0..1F3F0 ; Extended_Pictographic# 6.0 [17] (🏠..🏰) house..castle
|
||||
1F3F1..1F3F7 ; Extended_Pictographic# 7.0 [7] (🏱..🏷️) WHITE PENNANT..label
|
||||
1F3F8..1F3FA ; Extended_Pictographic# 8.0 [3] (🏸..🏺) badminton..amphora
|
||||
1F400..1F43E ; Extended_Pictographic# 6.0 [63] (🐀..🐾) rat..paw prints
|
||||
1F43F ; Extended_Pictographic# 7.0 [1] (🐿️) chipmunk
|
||||
1F440 ; Extended_Pictographic# 6.0 [1] (👀) eyes
|
||||
1F441 ; Extended_Pictographic# 7.0 [1] (👁️) eye
|
||||
1F442..1F4F7 ; Extended_Pictographic# 6.0[182] (👂..📷) ear..camera
|
||||
1F4F8 ; Extended_Pictographic# 7.0 [1] (📸) camera with flash
|
||||
1F4F9..1F4FC ; Extended_Pictographic# 6.0 [4] (📹..📼) video camera..videocassette
|
||||
1F4FD..1F4FE ; Extended_Pictographic# 7.0 [2] (📽️..📾) film projector..PORTABLE STEREO
|
||||
1F4FF ; Extended_Pictographic# 8.0 [1] (📿) prayer beads
|
||||
1F500..1F53D ; Extended_Pictographic# 6.0 [62] (🔀..🔽) shuffle tracks button..downwards button
|
||||
1F546..1F54A ; Extended_Pictographic# 7.0 [5] (🕆..🕊️) WHITE LATIN CROSS..dove
|
||||
1F54B..1F54F ; Extended_Pictographic# 8.0 [5] (🕋..🕏) kaaba..BOWL OF HYGIEIA
|
||||
1F550..1F567 ; Extended_Pictographic# 6.0 [24] (🕐..🕧) one o’clock..twelve-thirty
|
||||
1F568..1F579 ; Extended_Pictographic# 7.0 [18] (🕨..🕹️) RIGHT SPEAKER..joystick
|
||||
1F57A ; Extended_Pictographic# 9.0 [1] (🕺) man dancing
|
||||
1F57B..1F5A3 ; Extended_Pictographic# 7.0 [41] (🕻..🖣) LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX
|
||||
1F5A4 ; Extended_Pictographic# 9.0 [1] (🖤) black heart
|
||||
1F5A5..1F5FA ; Extended_Pictographic# 7.0 [86] (🖥️..🗺️) desktop computer..world map
|
||||
1F5FB..1F5FF ; Extended_Pictographic# 6.0 [5] (🗻..🗿) mount fuji..moai
|
||||
1F600 ; Extended_Pictographic# 6.1 [1] (😀) grinning face
|
||||
1F601..1F610 ; Extended_Pictographic# 6.0 [16] (😁..😐) beaming face with smiling eyes..neutral face
|
||||
1F611 ; Extended_Pictographic# 6.1 [1] (😑) expressionless face
|
||||
1F612..1F614 ; Extended_Pictographic# 6.0 [3] (😒..😔) unamused face..pensive face
|
||||
1F615 ; Extended_Pictographic# 6.1 [1] (😕) confused face
|
||||
1F616 ; Extended_Pictographic# 6.0 [1] (😖) confounded face
|
||||
1F617 ; Extended_Pictographic# 6.1 [1] (😗) kissing face
|
||||
1F618 ; Extended_Pictographic# 6.0 [1] (😘) face blowing a kiss
|
||||
1F619 ; Extended_Pictographic# 6.1 [1] (😙) kissing face with smiling eyes
|
||||
1F61A ; Extended_Pictographic# 6.0 [1] (😚) kissing face with closed eyes
|
||||
1F61B ; Extended_Pictographic# 6.1 [1] (😛) face with tongue
|
||||
1F61C..1F61E ; Extended_Pictographic# 6.0 [3] (😜..😞) winking face with tongue..disappointed face
|
||||
1F61F ; Extended_Pictographic# 6.1 [1] (😟) worried face
|
||||
1F620..1F625 ; Extended_Pictographic# 6.0 [6] (😠..😥) angry face..sad but relieved face
|
||||
1F626..1F627 ; Extended_Pictographic# 6.1 [2] (😦..😧) frowning face with open mouth..anguished face
|
||||
1F628..1F62B ; Extended_Pictographic# 6.0 [4] (😨..😫) fearful face..tired face
|
||||
1F62C ; Extended_Pictographic# 6.1 [1] (😬) grimacing face
|
||||
1F62D ; Extended_Pictographic# 6.0 [1] (😭) loudly crying face
|
||||
1F62E..1F62F ; Extended_Pictographic# 6.1 [2] (😮..😯) face with open mouth..hushed face
|
||||
1F630..1F633 ; Extended_Pictographic# 6.0 [4] (😰..😳) anxious face with sweat..flushed face
|
||||
1F634 ; Extended_Pictographic# 6.1 [1] (😴) sleeping face
|
||||
1F635..1F640 ; Extended_Pictographic# 6.0 [12] (😵..🙀) dizzy face..weary cat
|
||||
1F641..1F642 ; Extended_Pictographic# 7.0 [2] (🙁..🙂) slightly frowning face..slightly smiling face
|
||||
1F643..1F644 ; Extended_Pictographic# 8.0 [2] (🙃..🙄) upside-down face..face with rolling eyes
|
||||
1F645..1F64F ; Extended_Pictographic# 6.0 [11] (🙅..🙏) person gesturing NO..folded hands
|
||||
1F680..1F6C5 ; Extended_Pictographic# 6.0 [70] (🚀..🛅) rocket..left luggage
|
||||
1F6C6..1F6CF ; Extended_Pictographic# 7.0 [10] (🛆..🛏️) TRIANGLE WITH ROUNDED CORNERS..bed
|
||||
1F6D0 ; Extended_Pictographic# 8.0 [1] (🛐) place of worship
|
||||
1F6D1..1F6D2 ; Extended_Pictographic# 9.0 [2] (🛑..🛒) stop sign..shopping cart
|
||||
1F6D3..1F6D4 ; Extended_Pictographic# 10.0 [2] (🛓..🛔) STUPA..PAGODA
|
||||
1F6D5 ; Extended_Pictographic# 12.0 [1] (🛕) hindu temple
|
||||
1F6D6..1F6DF ; Extended_Pictographic# NA [10] (🛖..🛟) <reserved-1F6D6>..<reserved-1F6DF>
|
||||
1F6E0..1F6EC ; Extended_Pictographic# 7.0 [13] (🛠️..🛬) hammer and wrench..airplane arrival
|
||||
1F6ED..1F6EF ; Extended_Pictographic# NA [3] (..) <reserved-1F6ED>..<reserved-1F6EF>
|
||||
1F6F0..1F6F3 ; Extended_Pictographic# 7.0 [4] (🛰️..🛳️) satellite..passenger ship
|
||||
1F6F4..1F6F6 ; Extended_Pictographic# 9.0 [3] (🛴..🛶) kick scooter..canoe
|
||||
1F6F7..1F6F8 ; Extended_Pictographic# 10.0 [2] (🛷..🛸) sled..flying saucer
|
||||
1F6F9 ; Extended_Pictographic# 11.0 [1] (🛹) skateboard
|
||||
1F6FA ; Extended_Pictographic# 12.0 [1] (🛺) auto rickshaw
|
||||
1F6FB..1F6FF ; Extended_Pictographic# NA [5] (🛻..) <reserved-1F6FB>..<reserved-1F6FF>
|
||||
1F774..1F77F ; Extended_Pictographic# NA [12] (🝴..🝿) <reserved-1F774>..<reserved-1F77F>
|
||||
1F7D5..1F7D8 ; Extended_Pictographic# 11.0 [4] (🟕..🟘) CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE
|
||||
1F7D9..1F7DF ; Extended_Pictographic# NA [7] (🟙..) <reserved-1F7D9>..<reserved-1F7DF>
|
||||
1F7E0..1F7EB ; Extended_Pictographic# 12.0 [12] (🟠..🟫) orange circle..brown square
|
||||
1F7EC..1F7FF ; Extended_Pictographic# NA [20] (..) <reserved-1F7EC>..<reserved-1F7FF>
|
||||
1F80C..1F80F ; Extended_Pictographic# NA [4] (..) <reserved-1F80C>..<reserved-1F80F>
|
||||
1F848..1F84F ; Extended_Pictographic# NA [8] (..) <reserved-1F848>..<reserved-1F84F>
|
||||
1F85A..1F85F ; Extended_Pictographic# NA [6] (..) <reserved-1F85A>..<reserved-1F85F>
|
||||
1F888..1F88F ; Extended_Pictographic# NA [8] (..) <reserved-1F888>..<reserved-1F88F>
|
||||
1F8AE..1F8FF ; Extended_Pictographic# NA [82] (..) <reserved-1F8AE>..<reserved-1F8FF>
|
||||
1F90C ; Extended_Pictographic# NA [1] (🤌) <reserved-1F90C>
|
||||
1F90D..1F90F ; Extended_Pictographic# 12.0 [3] (🤍..🤏) white heart..pinching hand
|
||||
1F910..1F918 ; Extended_Pictographic# 8.0 [9] (🤐..🤘) zipper-mouth face..sign of the horns
|
||||
1F919..1F91E ; Extended_Pictographic# 9.0 [6] (🤙..🤞) call me hand..crossed fingers
|
||||
1F91F ; Extended_Pictographic# 10.0 [1] (🤟) love-you gesture
|
||||
1F920..1F927 ; Extended_Pictographic# 9.0 [8] (🤠..🤧) cowboy hat face..sneezing face
|
||||
1F928..1F92F ; Extended_Pictographic# 10.0 [8] (🤨..🤯) face with raised eyebrow..exploding head
|
||||
1F930 ; Extended_Pictographic# 9.0 [1] (🤰) pregnant woman
|
||||
1F931..1F932 ; Extended_Pictographic# 10.0 [2] (🤱..🤲) breast-feeding..palms up together
|
||||
1F933..1F93A ; Extended_Pictographic# 9.0 [8] (🤳..🤺) selfie..person fencing
|
||||
1F93C..1F93E ; Extended_Pictographic# 9.0 [3] (🤼..🤾) people wrestling..person playing handball
|
||||
1F93F ; Extended_Pictographic# 12.0 [1] (🤿) diving mask
|
||||
1F940..1F945 ; Extended_Pictographic# 9.0 [6] (🥀..🥅) wilted flower..goal net
|
||||
1F947..1F94B ; Extended_Pictographic# 9.0 [5] (🥇..🥋) 1st place medal..martial arts uniform
|
||||
1F94C ; Extended_Pictographic# 10.0 [1] (🥌) curling stone
|
||||
1F94D..1F94F ; Extended_Pictographic# 11.0 [3] (🥍..🥏) lacrosse..flying disc
|
||||
1F950..1F95E ; Extended_Pictographic# 9.0 [15] (🥐..🥞) croissant..pancakes
|
||||
1F95F..1F96B ; Extended_Pictographic# 10.0 [13] (🥟..🥫) dumpling..canned food
|
||||
1F96C..1F970 ; Extended_Pictographic# 11.0 [5] (🥬..🥰) leafy green..smiling face with hearts
|
||||
1F971 ; Extended_Pictographic# 12.0 [1] (🥱) yawning face
|
||||
1F972 ; Extended_Pictographic# NA [1] (🥲) <reserved-1F972>
|
||||
1F973..1F976 ; Extended_Pictographic# 11.0 [4] (🥳..🥶) partying face..cold face
|
||||
1F977..1F979 ; Extended_Pictographic# NA [3] (🥷..🥹) <reserved-1F977>..<reserved-1F979>
|
||||
1F97A ; Extended_Pictographic# 11.0 [1] (🥺) pleading face
|
||||
1F97B ; Extended_Pictographic# 12.0 [1] (🥻) sari
|
||||
1F97C..1F97F ; Extended_Pictographic# 11.0 [4] (🥼..🥿) lab coat..flat shoe
|
||||
1F980..1F984 ; Extended_Pictographic# 8.0 [5] (🦀..🦄) crab..unicorn
|
||||
1F985..1F991 ; Extended_Pictographic# 9.0 [13] (🦅..🦑) eagle..squid
|
||||
1F992..1F997 ; Extended_Pictographic# 10.0 [6] (🦒..🦗) giraffe..cricket
|
||||
1F998..1F9A2 ; Extended_Pictographic# 11.0 [11] (🦘..🦢) kangaroo..swan
|
||||
1F9A3..1F9A4 ; Extended_Pictographic# NA [2] (🦣..🦤) <reserved-1F9A3>..<reserved-1F9A4>
|
||||
1F9A5..1F9AA ; Extended_Pictographic# 12.0 [6] (🦥..🦪) sloth..oyster
|
||||
1F9AB..1F9AD ; Extended_Pictographic# NA [3] (🦫..🦭) <reserved-1F9AB>..<reserved-1F9AD>
|
||||
1F9AE..1F9AF ; Extended_Pictographic# 12.0 [2] (🦮..🦯) guide dog..probing cane
|
||||
1F9B0..1F9B9 ; Extended_Pictographic# 11.0 [10] (🦰..🦹) red hair..supervillain
|
||||
1F9BA..1F9BF ; Extended_Pictographic# 12.0 [6] (🦺..🦿) safety vest..mechanical leg
|
||||
1F9C0 ; Extended_Pictographic# 8.0 [1] (🧀) cheese wedge
|
||||
1F9C1..1F9C2 ; Extended_Pictographic# 11.0 [2] (🧁..🧂) cupcake..salt
|
||||
1F9C3..1F9CA ; Extended_Pictographic# 12.0 [8] (🧃..🧊) beverage box..ice cube
|
||||
1F9CB..1F9CC ; Extended_Pictographic# NA [2] (🧋..🧌) <reserved-1F9CB>..<reserved-1F9CC>
|
||||
1F9CD..1F9CF ; Extended_Pictographic# 12.0 [3] (🧍..🧏) person standing..deaf person
|
||||
1F9D0..1F9E6 ; Extended_Pictographic# 10.0 [23] (🧐..🧦) face with monocle..socks
|
||||
1F9E7..1F9FF ; Extended_Pictographic# 11.0 [25] (🧧..🧿) red envelope..nazar amulet
|
||||
1FA00..1FA53 ; Extended_Pictographic# 12.0 [84] (🨀..🩓) NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP
|
||||
1FA54..1FA5F ; Extended_Pictographic# NA [12] (..) <reserved-1FA54>..<reserved-1FA5F>
|
||||
1FA60..1FA6D ; Extended_Pictographic# 11.0 [14] (🩠..🩭) XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER
|
||||
1FA6E..1FA6F ; Extended_Pictographic# NA [2] (..) <reserved-1FA6E>..<reserved-1FA6F>
|
||||
1FA70..1FA73 ; Extended_Pictographic# 12.0 [4] (🩰..🩳) ballet shoes..shorts
|
||||
1FA74..1FA77 ; Extended_Pictographic# NA [4] (🩴..🩷) <reserved-1FA74>..<reserved-1FA77>
|
||||
1FA78..1FA7A ; Extended_Pictographic# 12.0 [3] (🩸..🩺) drop of blood..stethoscope
|
||||
1FA7B..1FA7F ; Extended_Pictographic# NA [5] (🩻..) <reserved-1FA7B>..<reserved-1FA7F>
|
||||
1FA80..1FA82 ; Extended_Pictographic# 12.0 [3] (🪀..🪂) yo-yo..parachute
|
||||
1FA83..1FA8F ; Extended_Pictographic# NA [13] (🪃..) <reserved-1FA83>..<reserved-1FA8F>
|
||||
1FA90..1FA95 ; Extended_Pictographic# 12.0 [6] (🪐..🪕) ringed planet..banjo
|
||||
1FA96..1FFFD ; Extended_Pictographic# NA[1384] (🪖..) <reserved-1FA96>..<reserved-1FFFD>
|
||||
|
||||
# Total elements: 3793
|
||||
|
||||
#EOF
|
|
@ -98,4 +98,35 @@ def code_change(_old_vsn, state, _extra) do
|
|||
defp update_emojis(emojis) do
|
||||
:ets.insert(@ets, emojis)
|
||||
end
|
||||
|
||||
@external_resource "lib/pleroma/emoji-data.txt"
|
||||
|
||||
emojis =
|
||||
@external_resource
|
||||
|> File.read!()
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end)
|
||||
|> Enum.map(fn line ->
|
||||
line
|
||||
|> String.split(";", parts: 2)
|
||||
|> hd()
|
||||
|> String.trim()
|
||||
|> String.split("..")
|
||||
|> case do
|
||||
[number] ->
|
||||
<<String.to_integer(number, 16)::utf8>>
|
||||
|
||||
[first, last] ->
|
||||
String.to_integer(first, 16)..String.to_integer(last, 16)
|
||||
|> Enum.map(&<<&1::utf8>>)
|
||||
end
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
for emoji <- emojis do
|
||||
def is_unicode_emoji?(unquote(emoji)), do: true
|
||||
end
|
||||
|
||||
def is_unicode_emoji?(_), do: false
|
||||
end
|
||||
|
|
|
@ -101,10 +101,28 @@ def following(%User{} = user) do
|
|||
|> select([r, u], u.follower_address)
|
||||
|> Repo.all()
|
||||
|
||||
if not user.local or user.nickname in [nil, "internal.fetch"] do
|
||||
if not user.local or user.invisible do
|
||||
following
|
||||
else
|
||||
[user.follower_address | following]
|
||||
end
|
||||
end
|
||||
|
||||
def move_following(origin, target) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> where(following_id: ^origin.id)
|
||||
|> where([r, f], f.allow_following_move == true)
|
||||
|> limit(50)
|
||||
|> preload([:follower])
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn following_relationship ->
|
||||
Repo.delete(following_relationship)
|
||||
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
|
||||
end)
|
||||
|> case do
|
||||
[] -> :ok
|
||||
_ -> move_following(origin, target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -616,6 +616,41 @@ def get_log_entry_message(%ModerationLog{
|
|||
"@#{actor_nickname} deleted status ##{subject_id}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "force_password_reset",
|
||||
"subject" => subjects
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "confirm_email",
|
||||
"subject" => subjects
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
def get_log_entry_message(%ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "resend_confirmation_email",
|
||||
"subject" => subjects
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} re-sent confirmation email for users: #{
|
||||
users_to_nicknames_string(subjects)
|
||||
}"
|
||||
end
|
||||
|
||||
defp nicknames_to_string(nicknames) do
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|
|
|
@ -280,11 +280,11 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow"] do
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow", "Move"] do
|
||||
notifications =
|
||||
activity
|
||||
|> get_notified_from_activity
|
||||
|> get_notified_from_activity()
|
||||
|> Enum.map(&create_notification(activity, &1))
|
||||
|
||||
{:ok, notifications}
|
||||
|
@ -311,19 +311,15 @@ def create_notification(%Activity{} = activity, %User{} = user) do
|
|||
|
||||
def get_notified_from_activity(activity, local_only \\ true)
|
||||
|
||||
def get_notified_from_activity(
|
||||
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
|
||||
local_only
|
||||
)
|
||||
when type in ["Create", "Like", "Announce", "Follow"] do
|
||||
recipients =
|
||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||
when type in ["Create", "Like", "Announce", "Follow", "Move"] do
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
|> Utils.maybe_notify_followers(activity)
|
||||
|> Enum.uniq()
|
||||
|
||||
User.get_users_from_set(recipients, local_only)
|
||||
|> User.get_users_from_set(local_only)
|
||||
end
|
||||
|
||||
def get_notified_from_activity(_, _local_only), do: []
|
||||
|
|
|
@ -63,7 +63,7 @@ def get_by_ap_id(ap_id) do
|
|||
end
|
||||
|
||||
defp warn_on_no_object_preloaded(ap_id) do
|
||||
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object"
|
||||
"Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
|
||||
|> Logger.debug()
|
||||
|
||||
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
|
||||
|
@ -255,4 +255,8 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
|
|||
|> Object.change(%{data: Map.merge(data || %{}, attrs)})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def local?(%Object{data: %{"id" => id}}) do
|
||||
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,15 +64,17 @@ def contain_origin(id, %{"actor" => _actor} = params) do
|
|||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||
do: contain_origin(id, Map.put(params, "actor", actor))
|
||||
|
||||
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
|
||||
def contain_origin(_id, _data), do: :error
|
||||
|
||||
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
|
||||
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)
|
||||
|
||||
compare_uris(id_uri, other_uri)
|
||||
end
|
||||
|
||||
def contain_origin_from_id(_id, _data), do: :error
|
||||
|
||||
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
||||
do: contain_origin(id, object)
|
||||
|
||||
|
|
|
@ -38,7 +38,8 @@ defp reinject_object(struct, data) do
|
|||
data <- maybe_reinject_internal_fields(data, struct),
|
||||
changeset <- Object.change(struct, %{data: data}),
|
||||
changeset <- touch_changeset(changeset),
|
||||
{:ok, object} <- Repo.insert_or_update(changeset) do
|
||||
{:ok, object} <- Repo.insert_or_update(changeset),
|
||||
{:ok, object} <- Object.set_cache(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
e ->
|
||||
|
@ -48,12 +49,12 @@ defp reinject_object(struct, data) do
|
|||
end
|
||||
|
||||
def refetch_object(%Object{data: %{"id" => id}} = object) do
|
||||
with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
|
||||
with {:local, false} <- {:local, Object.local?(object)},
|
||||
{:ok, data} <- fetch_and_contain_remote_object_from_id(id),
|
||||
{:ok, object} <- reinject_object(object, data) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:local, true} -> object
|
||||
{:local, true} -> {:ok, object}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,14 +16,28 @@ def secret_token do
|
|||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
||||
def call(%{params: %{"admin_token" => admin_token}} = conn, _) do
|
||||
if secret_token() && admin_token == secret_token() do
|
||||
conn
|
||||
|> assign(:user, %User{is_admin: true})
|
||||
def call(conn, _) do
|
||||
if secret_token() do
|
||||
authenticate(conn)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _), do: conn
|
||||
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
||||
if admin_token == secret_token() do
|
||||
assign(conn, :user, %User{is_admin: true})
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate(conn) do
|
||||
token = secret_token()
|
||||
|
||||
case get_req_header(conn, "x-admin-token") do
|
||||
[^token] -> assign(conn, :user, %User{is_admin: true})
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,7 +71,7 @@ defp fetch_user_and_token(token) do
|
|||
)
|
||||
|
||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||
with %Token{user: %{deactivated: false} = user} = token_record <- Repo.one(query) do
|
||||
with %Token{user: user} = token_record <- Repo.one(query) do
|
||||
{:ok, user, token_record}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.RateLimiter do
|
||||
@moduledoc """
|
||||
|
||||
## Configuration
|
||||
|
||||
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
|
||||
|
||||
* The first element: `scale` (Integer). The time scale in milliseconds.
|
||||
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
|
||||
|
||||
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
|
||||
|
||||
To disable a limiter set its value to `nil`.
|
||||
|
||||
### Example
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
one: {1000, 10},
|
||||
two: [{10_000, 10}, {10_000, 50}],
|
||||
foobar: nil
|
||||
|
||||
Here we have three limiters:
|
||||
|
||||
* `one` which is not over 10req/1s
|
||||
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
|
||||
* `foobar` which is disabled
|
||||
|
||||
## Usage
|
||||
|
||||
AllowedSyntax:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :limiter_name)
|
||||
plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
|
||||
|
||||
Allowed options:
|
||||
|
||||
* `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
|
||||
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
|
||||
|
||||
Inside a controller:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
|
||||
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
|
||||
when action in ~w(fav_status unfav_status)a
|
||||
)
|
||||
|
||||
or inside a router pipeline:
|
||||
|
||||
pipeline :api do
|
||||
...
|
||||
plug(Pleroma.Plugs.RateLimiter, :one)
|
||||
...
|
||||
end
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
def init(limiter_name) when is_atom(limiter_name) do
|
||||
init({limiter_name, []})
|
||||
end
|
||||
|
||||
def init({limiter_name, opts}) do
|
||||
case Pleroma.Config.get([:rate_limit, limiter_name]) do
|
||||
nil -> nil
|
||||
config -> {limiter_name, config, opts}
|
||||
end
|
||||
end
|
||||
|
||||
# Do not limit if there is no limiter configuration
|
||||
def call(conn, nil), do: conn
|
||||
|
||||
def call(conn, settings) do
|
||||
case check_rate(conn, settings) do
|
||||
{:ok, _count} ->
|
||||
conn
|
||||
|
||||
{:error, _count} ->
|
||||
render_throttled_error(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp bucket_name(conn, limiter_name, opts) do
|
||||
bucket_name = opts[:bucket_name] || limiter_name
|
||||
|
||||
if params_names = opts[:params] do
|
||||
params_values = for p <- Enum.sort(params_names), do: conn.params[p]
|
||||
Enum.join([bucket_name] ++ params_values, ":")
|
||||
else
|
||||
bucket_name
|
||||
end
|
||||
end
|
||||
|
||||
defp check_rate(
|
||||
%{assigns: %{user: %User{id: user_id}}} = conn,
|
||||
{limiter_name, [_, {scale, limit}], opts}
|
||||
) do
|
||||
bucket_name = bucket_name(conn, limiter_name, opts)
|
||||
ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
|
||||
end
|
||||
|
||||
defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
|
||||
bucket_name = bucket_name(conn, limiter_name, opts)
|
||||
ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
|
||||
end
|
||||
|
||||
defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
|
||||
check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
|
||||
end
|
||||
|
||||
def ip(%{remote_ip: remote_ip}) do
|
||||
remote_ip
|
||||
|> Tuple.to_list()
|
||||
|> Enum.join(".")
|
||||
end
|
||||
|
||||
defp render_throttled_error(conn) do
|
||||
conn
|
||||
|> render_error(:too_many_requests, "Throttled")
|
||||
|> halt()
|
||||
end
|
||||
end
|
44
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
Normal file
44
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
import Cachex.Spec
|
||||
|
||||
def start_link(init_arg) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def add_limiter(limiter_name, expiration) do
|
||||
{:ok, _pid} =
|
||||
DynamicSupervisor.start_child(
|
||||
__MODULE__,
|
||||
%{
|
||||
id: String.to_atom("rl_#{limiter_name}"),
|
||||
start:
|
||||
{Cachex, :start_link,
|
||||
[
|
||||
limiter_name,
|
||||
[
|
||||
expiration:
|
||||
expiration(
|
||||
default: expiration,
|
||||
interval: check_interval(expiration),
|
||||
lazy: true
|
||||
)
|
||||
]
|
||||
]}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_init_arg) do
|
||||
DynamicSupervisor.init(strategy: :one_for_one)
|
||||
end
|
||||
|
||||
defp check_interval(exp) do
|
||||
(exp / 2)
|
||||
|> Kernel.trunc()
|
||||
|> Kernel.min(5000)
|
||||
|> Kernel.max(1)
|
||||
end
|
||||
end
|
227
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
Normal file
227
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
Normal file
|
@ -0,0 +1,227 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.RateLimiter do
|
||||
@moduledoc """
|
||||
|
||||
## Configuration
|
||||
|
||||
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
|
||||
|
||||
* The first element: `scale` (Integer). The time scale in milliseconds.
|
||||
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
|
||||
|
||||
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
|
||||
|
||||
To disable a limiter set its value to `nil`.
|
||||
|
||||
### Example
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
one: {1000, 10},
|
||||
two: [{10_000, 10}, {10_000, 50}],
|
||||
foobar: nil
|
||||
|
||||
Here we have three limiters:
|
||||
|
||||
* `one` which is not over 10req/1s
|
||||
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
|
||||
* `foobar` which is disabled
|
||||
|
||||
## Usage
|
||||
|
||||
AllowedSyntax:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
|
||||
plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
|
||||
|
||||
Allowed options:
|
||||
|
||||
* `name` required, always used to fetch the limit values from the config
|
||||
* `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
|
||||
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
|
||||
|
||||
Inside a controller:
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
|
||||
when action in ~w(fav_status unfav_status)a
|
||||
)
|
||||
|
||||
or inside a router pipeline:
|
||||
|
||||
pipeline :api do
|
||||
...
|
||||
plug(Pleroma.Plugs.RateLimiter, name: :one)
|
||||
...
|
||||
end
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
|
||||
alias Pleroma.User
|
||||
|
||||
def init(opts) do
|
||||
limiter_name = Keyword.get(opts, :name)
|
||||
|
||||
case Pleroma.Config.get([:rate_limit, limiter_name]) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
config ->
|
||||
name_root = Keyword.get(opts, :bucket_name, limiter_name)
|
||||
|
||||
%{
|
||||
name: name_root,
|
||||
limits: config,
|
||||
opts: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Do not limit if there is no limiter configuration
|
||||
def call(conn, nil), do: conn
|
||||
|
||||
def call(conn, settings) do
|
||||
settings
|
||||
|> incorporate_conn_info(conn)
|
||||
|> check_rate()
|
||||
|> case do
|
||||
{:ok, _count} ->
|
||||
conn
|
||||
|
||||
{:error, _count} ->
|
||||
render_throttled_error(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_bucket(conn, name_root, settings) do
|
||||
settings =
|
||||
settings
|
||||
|> incorporate_conn_info(conn)
|
||||
|
||||
bucket_name = make_bucket_name(%{settings | name: name_root})
|
||||
key_name = make_key_name(settings)
|
||||
limit = get_limits(settings)
|
||||
|
||||
case Cachex.get(bucket_name, key_name) do
|
||||
{:error, :no_cache} ->
|
||||
{:err, :not_found}
|
||||
|
||||
{:ok, nil} ->
|
||||
{0, limit}
|
||||
|
||||
{:ok, value} ->
|
||||
{value, limit - value}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_rate(settings) do
|
||||
bucket_name = make_bucket_name(settings)
|
||||
key_name = make_key_name(settings)
|
||||
limit = get_limits(settings)
|
||||
|
||||
case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
|
||||
{:commit, value} ->
|
||||
{:ok, value}
|
||||
|
||||
{:ignore, value} ->
|
||||
{:error, value}
|
||||
|
||||
{:error, :no_cache} ->
|
||||
initialize_buckets(settings)
|
||||
check_rate(settings)
|
||||
end
|
||||
end
|
||||
|
||||
defp increment_value(nil, _limit), do: {:commit, 1}
|
||||
|
||||
defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
|
||||
|
||||
defp increment_value(val, _limit), do: {:commit, val + 1}
|
||||
|
||||
defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
|
||||
Map.merge(settings, %{
|
||||
mode: :user,
|
||||
conn_params: params,
|
||||
conn_info: "#{user_id}"
|
||||
})
|
||||
end
|
||||
|
||||
defp incorporate_conn_info(settings, %{params: params} = conn) do
|
||||
Map.merge(settings, %{
|
||||
mode: :anon,
|
||||
conn_params: params,
|
||||
conn_info: "#{ip(conn)}"
|
||||
})
|
||||
end
|
||||
|
||||
defp ip(%{remote_ip: remote_ip}) do
|
||||
remote_ip
|
||||
|> Tuple.to_list()
|
||||
|> Enum.join(".")
|
||||
end
|
||||
|
||||
defp render_throttled_error(conn) do
|
||||
conn
|
||||
|> render_error(:too_many_requests, "Throttled")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp make_key_name(settings) do
|
||||
""
|
||||
|> attach_params(settings)
|
||||
|> attach_identity(settings)
|
||||
end
|
||||
|
||||
defp get_scale(_, {scale, _}), do: scale
|
||||
|
||||
defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
|
||||
|
||||
defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
|
||||
|
||||
defp get_limits(%{limits: {_scale, limit}}), do: limit
|
||||
|
||||
defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
|
||||
|
||||
defp get_limits(%{limits: [{_, limit}, _]}), do: limit
|
||||
|
||||
defp make_bucket_name(%{mode: :user, name: name_root}),
|
||||
do: user_bucket_name(name_root)
|
||||
|
||||
defp make_bucket_name(%{mode: :anon, name: name_root}),
|
||||
do: anon_bucket_name(name_root)
|
||||
|
||||
defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
|
||||
param_string =
|
||||
opts
|
||||
|> Keyword.get(:params, [])
|
||||
|> Enum.sort()
|
||||
|> Enum.map(&Map.get(conn_params, &1, ""))
|
||||
|> Enum.join(":")
|
||||
|
||||
"#{input}#{param_string}"
|
||||
end
|
||||
|
||||
defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
|
||||
|
||||
defp initialize_buckets(%{name: name, limits: limits}) do
|
||||
LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
|
||||
LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
|
||||
end
|
||||
|
||||
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
|
||||
do: "user:#{base}:#{conn_info}"
|
||||
|
||||
defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
|
||||
do: "ip:#{base}:#{conn_info}"
|
||||
|
||||
defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
|
||||
defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
|
||||
end
|
16
lib/pleroma/plugs/rate_limiter/supervisor.ex
Normal file
16
lib/pleroma/plugs/rate_limiter/supervisor.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Pleroma.Plugs.RateLimiter.Supervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_args) do
|
||||
children = [
|
||||
Pleroma.Plugs.RateLimiter.LimiterSupervisor
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
|
||||
Supervisor.init(children, opts)
|
||||
end
|
||||
end
|
26
lib/pleroma/plugs/static_fe_plug.ex
Normal file
26
lib/pleroma/plugs/static_fe_plug.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.StaticFEPlug do
|
||||
import Plug.Conn
|
||||
alias Pleroma.Web.StaticFE.StaticFEController
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(conn, _) do
|
||||
if enabled?() and accepts_html?(conn) do
|
||||
conn
|
||||
|> StaticFEController.call(:show)
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
|
||||
|
||||
defp accepts_html?(conn) do
|
||||
conn |> get_req_header("accept") |> List.first() |> String.contains?("text/html")
|
||||
end
|
||||
end
|
|
@ -24,7 +24,8 @@ defmodule Pleroma.Plugs.TrailingFormatPlug do
|
|||
"/api/help",
|
||||
"/api/externalprofile",
|
||||
"/notice",
|
||||
"/api/pleroma/emoji"
|
||||
"/api/pleroma/emoji",
|
||||
"/api/oauth_tokens"
|
||||
]
|
||||
|
||||
def init(opts) do
|
||||
|
|
|
@ -10,10 +10,14 @@ def init(options) do
|
|||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{deactivated: true}}} = conn, _) do
|
||||
def call(%{assigns: %{user: %User{} = user}} = conn, _) do
|
||||
if User.auth_active?(user) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> assign(:user, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|
|
|
@ -67,8 +67,7 @@ defmodule Pleroma.User do
|
|||
field(:source_data, :map, default: %{})
|
||||
field(:note_count, :integer, default: 0)
|
||||
field(:follower_count, :integer, default: 0)
|
||||
# Should be filled in only for remote users
|
||||
field(:following_count, :integer, default: nil)
|
||||
field(:following_count, :integer, default: 0)
|
||||
field(:locked, :boolean, default: false)
|
||||
field(:confirmation_pending, :boolean, default: false)
|
||||
field(:password_reset_pending, :boolean, default: false)
|
||||
|
@ -104,7 +103,9 @@ defmodule Pleroma.User do
|
|||
field(:raw_fields, {:array, :map}, default: [])
|
||||
field(:discoverable, :boolean, default: false)
|
||||
field(:invisible, :boolean, default: false)
|
||||
field(:allow_following_move, :boolean, default: true)
|
||||
field(:skip_thread_containment, :boolean, default: false)
|
||||
field(:also_known_as, {:array, :string}, default: [])
|
||||
|
||||
field(:notification_settings, :map,
|
||||
default: %{
|
||||
|
@ -119,11 +120,12 @@ defmodule Pleroma.User do
|
|||
has_many(:registrations, Registration)
|
||||
has_many(:deliveries, Delivery)
|
||||
|
||||
field(:info, :map, default: %{})
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc "Returns if the user should be allowed to authenticate"
|
||||
def auth_active?(%User{deactivated: true}), do: false
|
||||
|
||||
def auth_active?(%User{confirmation_pending: true}),
|
||||
do: !Pleroma.Config.get([:instance, :account_activation_required])
|
||||
|
||||
|
@ -131,6 +133,8 @@ def auth_active?(%User{}), do: true
|
|||
|
||||
def visible_for?(user, for_user \\ nil)
|
||||
|
||||
def visible_for?(%User{invisible: true}, _), do: false
|
||||
|
||||
def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
|
||||
|
||||
def visible_for?(%User{} = user, for_user) do
|
||||
|
@ -173,22 +177,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
|
|||
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
|
||||
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
|
||||
|
||||
def user_info(%User{} = user, args \\ %{}) do
|
||||
following_count =
|
||||
Map.get(args, :following_count, user.following_count || following_count(user))
|
||||
|
||||
follower_count = Map.get(args, :follower_count, user.follower_count)
|
||||
|
||||
%{
|
||||
note_count: user.note_count,
|
||||
locked: user.locked,
|
||||
confirmation_pending: user.confirmation_pending,
|
||||
default_scope: user.default_scope
|
||||
}
|
||||
|> Map.put(:following_count, following_count)
|
||||
|> Map.put(:follower_count, follower_count)
|
||||
end
|
||||
|
||||
def follow_state(%User{} = user, %User{} = target) do
|
||||
case Utils.fetch_latest_follow(user, target) do
|
||||
%{data: %{"state" => state}} -> state
|
||||
|
@ -207,10 +195,6 @@ def set_follow_state_cache(user_ap_id, target_ap_id, state) do
|
|||
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
|
||||
end
|
||||
|
||||
def set_info_cache(user, args) do
|
||||
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
|
||||
end
|
||||
|
||||
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
def restrict_deactivated(query) do
|
||||
from(u in query, where: u.deactivated != ^true)
|
||||
|
@ -241,7 +225,6 @@ def remote_user_creation(params) do
|
|||
|
||||
params =
|
||||
params
|
||||
|> Map.put(:info, params[:info] || %{})
|
||||
|> truncate_if_exists(:name, name_limit)
|
||||
|> truncate_if_exists(:bio, bio_limit)
|
||||
|> truncate_fields_param()
|
||||
|
@ -270,7 +253,8 @@ def remote_user_creation(params) do
|
|||
:fields,
|
||||
:following_count,
|
||||
:discoverable,
|
||||
:invisible
|
||||
:invisible,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|
@ -312,13 +296,15 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:hide_favorites,
|
||||
:allow_following_move,
|
||||
:background,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:fields,
|
||||
:raw_fields,
|
||||
:pleroma_settings_store,
|
||||
:discoverable
|
||||
:discoverable,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
@ -356,9 +342,11 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|
|||
:hide_follows,
|
||||
:fields,
|
||||
:hide_followers,
|
||||
:allow_following_move,
|
||||
:discoverable,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count
|
||||
:hide_follows_count,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
@ -489,6 +477,10 @@ def try_send_confirmation_email(%User{} = user) do
|
|||
end
|
||||
end
|
||||
|
||||
def try_send_confirmation_email(users) do
|
||||
Enum.each(users, &try_send_confirmation_email/1)
|
||||
end
|
||||
|
||||
def needs_update?(%User{local: true}), do: false
|
||||
|
||||
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
|
||||
|
@ -519,14 +511,9 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
|
|||
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
|
||||
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
|
||||
def follow_all(follower, followeds) do
|
||||
followeds =
|
||||
Enum.reject(followeds, fn followed ->
|
||||
blocks?(follower, followed) || blocks?(followed, follower)
|
||||
end)
|
||||
|
||||
Enum.each(followeds, &follow(follower, &1, "accept"))
|
||||
|
||||
Enum.each(followeds, &update_follower_count/1)
|
||||
followeds
|
||||
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|
||||
|> Enum.each(&follow(follower, &1, "accept"))
|
||||
|
||||
set_cache(follower)
|
||||
end
|
||||
|
@ -546,11 +533,11 @@ def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
|
|||
true ->
|
||||
FollowingRelationship.follow(follower, followed, state)
|
||||
|
||||
follower = maybe_update_following_count(follower)
|
||||
|
||||
{:ok, _} = update_follower_count(followed)
|
||||
|
||||
set_cache(follower)
|
||||
follower
|
||||
|> update_following_count()
|
||||
|> set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -558,11 +545,12 @@ def unfollow(%User{} = follower, %User{} = followed) do
|
|||
if following?(follower, followed) and follower.ap_id != followed.ap_id do
|
||||
FollowingRelationship.unfollow(follower, followed)
|
||||
|
||||
follower = maybe_update_following_count(follower)
|
||||
|
||||
{:ok, followed} = update_follower_count(followed)
|
||||
|
||||
set_cache(follower)
|
||||
{:ok, follower} =
|
||||
follower
|
||||
|> update_following_count()
|
||||
|> set_cache()
|
||||
|
||||
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
|
||||
else
|
||||
|
@ -612,7 +600,6 @@ def set_cache({:error, err}), do: {:error, err}
|
|||
def set_cache(%User{} = user) do
|
||||
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
|
||||
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
|
||||
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
|
@ -631,7 +618,6 @@ def update_and_set_cache(changeset) do
|
|||
def invalidate_cache(user) do
|
||||
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
|
||||
Cachex.del(:user_cache, "nickname:#{user.nickname}")
|
||||
Cachex.del(:user_cache, "user_info:#{user.id}")
|
||||
end
|
||||
|
||||
def get_cached_by_ap_id(ap_id) do
|
||||
|
@ -699,11 +685,6 @@ def get_by_nickname_or_email(nickname_or_email) do
|
|||
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
|
||||
end
|
||||
|
||||
def get_cached_user_info(user) do
|
||||
key = "user_info:#{user.id}"
|
||||
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
|
||||
end
|
||||
|
||||
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
|
||||
|
||||
def get_or_fetch_by_nickname(nickname) do
|
||||
|
@ -892,8 +873,8 @@ def update_follower_count(%User{} = user) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec maybe_update_following_count(User.t()) :: User.t()
|
||||
def maybe_update_following_count(%User{local: false} = user) do
|
||||
@spec update_following_count(User.t()) :: User.t()
|
||||
def update_following_count(%User{local: false} = user) do
|
||||
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
maybe_fetch_follow_information(user)
|
||||
else
|
||||
|
@ -901,7 +882,13 @@ def maybe_update_following_count(%User{local: false} = user) do
|
|||
end
|
||||
end
|
||||
|
||||
def maybe_update_following_count(user), do: user
|
||||
def update_following_count(%User{local: true} = user) do
|
||||
following_count = FollowingRelationship.following_count(user)
|
||||
|
||||
user
|
||||
|> follow_information_changeset(%{following_count: following_count})
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
def set_unread_conversation_count(%User{local: true} = user) do
|
||||
unread_query = Participation.unread_conversation_count_for_user(user)
|
||||
|
@ -1094,7 +1081,12 @@ def deactivate(users, status) when is_list(users) do
|
|||
|
||||
def deactivate(%User{} = user, status) do
|
||||
with {:ok, user} <- set_activation_status(user, status) do
|
||||
Enum.each(get_followers(user), &invalidate_cache/1)
|
||||
user
|
||||
|> get_followers()
|
||||
|> Enum.filter(& &1.local)
|
||||
|> Enum.each(fn follower ->
|
||||
follower |> update_following_count() |> set_cache()
|
||||
end)
|
||||
|
||||
# Only update local user counts, remote will be update during the next pull.
|
||||
user
|
||||
|
@ -1223,7 +1215,7 @@ def external_users_query do
|
|||
def external_users(opts \\ []) do
|
||||
query =
|
||||
external_users_query()
|
||||
|> select([u], struct(u, [:id, :ap_id, :info]))
|
||||
|> select([u], struct(u, [:id, :ap_id]))
|
||||
|
||||
query =
|
||||
if opts[:max_id],
|
||||
|
@ -1314,19 +1306,20 @@ def get_or_fetch_by_ap_id(ap_id) do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
|
||||
@doc """
|
||||
Creates an internal service actor by URI if missing.
|
||||
Optionally takes nickname for addressing.
|
||||
"""
|
||||
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
|
||||
with %User{} = user <- get_cached_by_ap_id(uri) do
|
||||
user
|
||||
else
|
||||
_ ->
|
||||
with user when is_nil(user) <- get_cached_by_ap_id(uri) do
|
||||
{:ok, user} =
|
||||
%User{}
|
||||
|> cast(%{}, [:ap_id, :nickname, :local])
|
||||
|> put_change(:ap_id, uri)
|
||||
|> put_change(:nickname, nickname)
|
||||
|> put_change(:local, true)
|
||||
|> put_change(:follower_address, uri <> "/followers")
|
||||
%User{
|
||||
invisible: true,
|
||||
local: true,
|
||||
ap_id: uri,
|
||||
nickname: nickname,
|
||||
follower_address: uri <> "/followers"
|
||||
}
|
||||
|> Repo.insert()
|
||||
|
||||
user
|
||||
|
@ -1572,6 +1565,11 @@ def toggle_confirmation(%User{} = user) do
|
|||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
@spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
|
||||
def toggle_confirmation(users) do
|
||||
Enum.map(users, &toggle_confirmation/1)
|
||||
end
|
||||
|
||||
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
|
||||
mascot
|
||||
end
|
||||
|
|
|
@ -45,6 +45,7 @@ defp search_query(query_string, for_user, following) do
|
|||
for_user
|
||||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_invisible_users()
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> trigram_rank(query_string)
|
||||
|
@ -98,6 +99,10 @@ defp trigram_rank(query, query_string) do
|
|||
defp base_query(_user, false), do: User
|
||||
defp base_query(user, true), do: User.get_followers_query(user)
|
||||
|
||||
defp filter_invisible_users(query) do
|
||||
from(q in query, where: q.invisible == false)
|
||||
end
|
||||
|
||||
defp filter_blocked_user(query, %User{blocks: blocks})
|
||||
when length(blocks) > 0 do
|
||||
from(q in query, where: not (q.ap_id in ^blocks))
|
||||
|
|
|
@ -322,6 +322,32 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def react_with_emoji(user, object, emoji, options \\ []) do
|
||||
with local <- Keyword.get(options, :local, true),
|
||||
activity_id <- Keyword.get(options, :activity_id, nil),
|
||||
Pleroma.Emoji.is_unicode_emoji?(emoji),
|
||||
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
|
||||
{:ok, activity} <- insert(reaction_data, local),
|
||||
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
def unreact_with_emoji(user, reaction_id, options \\ []) do
|
||||
with local <- Keyword.get(options, :local, true),
|
||||
activity_id <- Keyword.get(options, :activity_id, nil),
|
||||
user_ap_id <- user.ap_id,
|
||||
%Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
|
||||
object <- Object.normalize(reaction_activity),
|
||||
unreact_data <- make_undo_data(user, reaction_activity, activity_id),
|
||||
{:ok, activity} <- insert(unreact_data, local),
|
||||
{:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
|
||||
def like(
|
||||
%User{ap_id: ap_id} = user,
|
||||
|
@ -515,6 +541,30 @@ def flag(
|
|||
end
|
||||
end
|
||||
|
||||
def move(%User{} = origin, %User{} = target, local \\ true) do
|
||||
params = %{
|
||||
"type" => "Move",
|
||||
"actor" => origin.ap_id,
|
||||
"object" => origin.ap_id,
|
||||
"target" => target.ap_id
|
||||
}
|
||||
|
||||
with true <- origin.ap_id in target.also_known_as,
|
||||
{:ok, activity} <- insert(params, local) do
|
||||
maybe_federate(activity)
|
||||
|
||||
BackgroundWorker.enqueue("move_following", %{
|
||||
"origin_id" => origin.id,
|
||||
"target_id" => target.id
|
||||
})
|
||||
|
||||
{:ok, activity}
|
||||
else
|
||||
false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_activities_for_context_query(context, opts) do
|
||||
public = [Pleroma.Constants.as_public()]
|
||||
|
||||
|
@ -568,7 +618,6 @@ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|
|||
|> fetch_activities_query(opts)
|
||||
|> restrict_unlisted()
|
||||
|> Pagination.fetch_paginated(opts, pagination)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
@ -709,6 +758,17 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|
|||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def fetch_instance_activities(params) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("instance", params["instance"])
|
||||
|> Map.put("whole_db", true)
|
||||
|
||||
fetch_activities([Pleroma.Constants.as_public()], params, :offset)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp user_activities_recipients(%{"godmode" => true}) do
|
||||
[]
|
||||
end
|
||||
|
@ -936,6 +996,20 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
|
|||
|
||||
defp restrict_muted_reblogs(query, _), do: query
|
||||
|
||||
defp restrict_instance(query, %{"instance" => instance}) do
|
||||
users =
|
||||
from(
|
||||
u in User,
|
||||
select: u.ap_id,
|
||||
where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
from(activity in query, where: activity.actor in ^users)
|
||||
end
|
||||
|
||||
defp restrict_instance(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
@ -1016,6 +1090,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_reblogs(opts)
|
||||
|> restrict_pinned(opts)
|
||||
|> restrict_muted_reblogs(opts)
|
||||
|> restrict_instance(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_visibility(opts)
|
||||
|
@ -1120,7 +1195,8 @@ defp object_to_user_data(data) do
|
|||
name: data["name"],
|
||||
follower_address: data["followers"],
|
||||
following_address: data["following"],
|
||||
bio: data["summary"]
|
||||
bio: data["summary"],
|
||||
also_known_as: Map.get(data, "alsoKnownAs", [])
|
||||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
|
@ -1182,13 +1258,13 @@ defp maybe_update_follow_information(data) do
|
|||
end
|
||||
end
|
||||
|
||||
defp collection_private(data) do
|
||||
if is_map(data["first"]) and
|
||||
data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
|
||||
defp collection_private(%{"first" => first}) do
|
||||
if is_map(first) and
|
||||
first["type"] in ["CollectionPage", "OrderedCollectionPage"] do
|
||||
{:ok, false}
|
||||
else
|
||||
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(first) do
|
||||
{:ok, false}
|
||||
else
|
||||
{:error, {:ok, %{status: code}}} when code in [401, 403] ->
|
||||
|
@ -1203,6 +1279,8 @@ defp collection_private(data) do
|
|||
end
|
||||
end
|
||||
|
||||
defp collection_private(_data), do: {:ok, true}
|
||||
|
||||
def user_data_from_user_object(data) do
|
||||
with {:ok, data} <- MRF.filter(data),
|
||||
{:ok, data} <- object_to_user_data(data) do
|
||||
|
|
|
@ -45,7 +45,7 @@ def relay_active?(conn, _) do
|
|||
end
|
||||
|
||||
def user(conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- User.ensure_keys_present(user) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|
@ -53,6 +53,7 @@ def user(conn, %{"nickname" => nickname}) do
|
|||
|> render("user.json", %{user: user})
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
%{local: false} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
101
lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
Normal file
101
lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@moduledoc "Filter activities depending on their age"
|
||||
@behaviour MRF
|
||||
|
||||
defp check_date(%{"published" => published} = message) do
|
||||
with %DateTime{} = now <- DateTime.utc_now(),
|
||||
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
|
||||
max_ttl <- Config.get([:mrf_object_age, :threshold]),
|
||||
{:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
|
||||
{:ok, message}
|
||||
else
|
||||
{:ttl, true} ->
|
||||
{:reject, nil}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_reject(message, actions) do
|
||||
if :reject in actions do
|
||||
{:reject, nil}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_delist(message, actions) do
|
||||
if :delist in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
|
||||
cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
|
||||
|
||||
message =
|
||||
message
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_strip_followers(message, actions) do
|
||||
if :strip_followers in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
to = List.delete(message["to"], user.follower_address)
|
||||
cc = List.delete(message["cc"], user.follower_address)
|
||||
|
||||
message =
|
||||
message
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "published" => _} = message) do
|
||||
with actions <- Config.get([:mrf_object_age, :actions]),
|
||||
{:reject, _} <- check_date(message),
|
||||
{:ok, message} <- check_reject(message, actions),
|
||||
{:ok, message} <- check_delist(message, actions),
|
||||
{:ok, message} <- check_strip_followers(message, actions) do
|
||||
{:ok, message}
|
||||
else
|
||||
# check_date() is allowed to short-circuit the pipeline
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
|
@ -14,7 +14,6 @@ def get_actor do
|
|||
relay_ap_id()
|
||||
|> User.get_or_create_service_actor_by_ap_id()
|
||||
|
||||
{:ok, actor} = User.set_invisible(actor, true)
|
||||
actor
|
||||
end
|
||||
|
||||
|
|
|
@ -566,6 +566,34 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
@misskey_reactions %{
|
||||
"like" => "👍",
|
||||
"love" => "❤️",
|
||||
"laugh" => "😆",
|
||||
"hmm" => "🤔",
|
||||
"surprise" => "😮",
|
||||
"congrats" => "🎉",
|
||||
"angry" => "💢",
|
||||
"confused" => "😥",
|
||||
"rip" => "😇",
|
||||
"pudding" => "🍮",
|
||||
"star" => "⭐"
|
||||
}
|
||||
|
||||
@doc "Rewrite misskey likes into EmojiReactions"
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"_misskey_reaction" => reaction
|
||||
} = data,
|
||||
options
|
||||
) do
|
||||
data
|
||||
|> Map.put("type", "EmojiReaction")
|
||||
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|
||||
|> handle_incoming(options)
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
|
@ -580,6 +608,27 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "EmojiReaction",
|
||||
"object" => object_id,
|
||||
"actor" => _actor,
|
||||
"id" => id,
|
||||
"content" => emoji
|
||||
} = data,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:ok, object} <- get_obj_helper(object_id),
|
||||
{:ok, activity, _object} <-
|
||||
ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
|
@ -620,7 +669,7 @@ def handle_incoming(
|
|||
|
||||
update_data =
|
||||
new_user_data
|
||||
|> Map.take([:avatar, :banner, :bio, :name])
|
||||
|> Map.take([:avatar, :banner, :bio, :name, :also_known_as])
|
||||
|> Map.put(:fields, fields)
|
||||
|> Map.put(:locked, locked)
|
||||
|> Map.put(:invisible, invisible)
|
||||
|
@ -715,6 +764,28 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "EmojiReaction", "id" => reaction_activity_id},
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:ok, activity, _} <-
|
||||
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
|
||||
activity_id: id,
|
||||
local: false
|
||||
) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
|
@ -786,6 +857,24 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
|
||||
true <- origin_actor in target_user.also_known_as do
|
||||
ActivityPub.move(origin_user, target_user, false)
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(_, _), do: :error
|
||||
|
||||
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||
|
@ -1048,7 +1137,7 @@ def prepare_attachments(object) do
|
|||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
||||
defp strip_internal_fields(object) do
|
||||
def strip_internal_fields(object) do
|
||||
object
|
||||
|> Map.drop(Pleroma.Constants.object_internal_fields())
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
@ -255,6 +256,16 @@ def get_existing_like(actor, %{data: %{"id" => id}}) do
|
|||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns like activities targeting an object
|
||||
"""
|
||||
def get_object_likes(%{data: %{"id" => id}}) do
|
||||
id
|
||||
|> Activity.Queries.by_object_id()
|
||||
|> Activity.Queries.by_type("Like")
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec make_like_data(User.t(), map(), String.t()) :: map()
|
||||
def make_like_data(
|
||||
%User{ap_id: ap_id} = actor,
|
||||
|
@ -286,13 +297,30 @@ def make_like_data(
|
|||
|> maybe_put("id", activity_id)
|
||||
end
|
||||
|
||||
def make_emoji_reaction_data(user, object, emoji, activity_id) do
|
||||
make_like_data(user, object, activity_id)
|
||||
|> Map.put("type", "EmojiReaction")
|
||||
|> Map.put("content", emoji)
|
||||
end
|
||||
|
||||
@spec update_element_in_object(String.t(), list(any), Object.t()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_element_in_object(property, element, object) do
|
||||
length =
|
||||
if is_map(element) do
|
||||
element
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> length()
|
||||
else
|
||||
element
|
||||
|> length()
|
||||
end
|
||||
|
||||
data =
|
||||
Map.merge(
|
||||
object.data,
|
||||
%{"#{property}_count" => length(element), "#{property}s" => element}
|
||||
%{"#{property}_count" => length, "#{property}s" => element}
|
||||
)
|
||||
|
||||
object
|
||||
|
@ -300,6 +328,38 @@ def update_element_in_object(property, element, object) do
|
|||
|> Object.update_and_set_cache()
|
||||
end
|
||||
|
||||
@spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
|
||||
def add_emoji_reaction_to_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
object
|
||||
) do
|
||||
reactions = object.data["reactions"] || %{}
|
||||
emoji_actors = reactions[emoji] || []
|
||||
new_emoji_actors = [actor | emoji_actors] |> Enum.uniq()
|
||||
new_reactions = Map.put(reactions, emoji, new_emoji_actors)
|
||||
update_element_in_object("reaction", new_reactions, object)
|
||||
end
|
||||
|
||||
def remove_emoji_reaction_from_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
object
|
||||
) do
|
||||
reactions = object.data["reactions"] || %{}
|
||||
emoji_actors = reactions[emoji] || []
|
||||
new_emoji_actors = List.delete(emoji_actors, actor)
|
||||
|
||||
new_reactions =
|
||||
if new_emoji_actors == [] do
|
||||
Map.delete(reactions, emoji)
|
||||
else
|
||||
Map.put(reactions, emoji, new_emoji_actors)
|
||||
end
|
||||
|
||||
update_element_in_object("reaction", new_reactions, object)
|
||||
end
|
||||
|
||||
@spec add_like_to_object(Activity.t(), Object.t()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
|
||||
|
@ -397,6 +457,19 @@ def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
|
|||
|> Repo.one()
|
||||
end
|
||||
|
||||
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
|
||||
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
|
||||
|
||||
"EmojiReaction"
|
||||
|> Activity.Queries.by_type()
|
||||
|> where(actor: ^ap_id)
|
||||
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|
||||
|> Activity.Queries.by_object_id(object_ap_id)
|
||||
|> order_by([activity], fragment("? desc nulls last", activity.id))
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
#### Announce-related helpers
|
||||
|
||||
@doc """
|
||||
|
@ -489,6 +562,25 @@ def make_unlike_data(
|
|||
|> maybe_put("id", activity_id)
|
||||
end
|
||||
|
||||
def make_undo_data(
|
||||
%User{ap_id: actor, follower_address: follower_address},
|
||||
%Activity{
|
||||
data: %{"id" => undone_activity_id, "context" => context},
|
||||
actor: undone_activity_actor
|
||||
},
|
||||
activity_id \\ nil
|
||||
) do
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"actor" => actor,
|
||||
"object" => undone_activity_id,
|
||||
"to" => [follower_address, undone_activity_actor],
|
||||
"cc" => [Pleroma.Constants.as_public()],
|
||||
"context" => context
|
||||
}
|
||||
|> maybe_put("id", activity_id)
|
||||
end
|
||||
|
||||
@spec add_announce_to_object(Activity.t(), Object.t()) ::
|
||||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_announce_to_object(
|
||||
|
@ -615,8 +707,14 @@ def make_flag_data(%{actor: actor, context: context, content: content} = params,
|
|||
def make_flag_data(_, _), do: %{}
|
||||
|
||||
defp build_flag_object(%{account: account, statuses: statuses} = _) do
|
||||
[account.ap_id] ++
|
||||
Enum.map(statuses || [], fn act ->
|
||||
[account.ap_id] ++ build_flag_object(%{statuses: statuses})
|
||||
end
|
||||
|
||||
defp build_flag_object(%{statuses: statuses}) do
|
||||
Enum.map(statuses || [], &build_flag_object/1)
|
||||
end
|
||||
|
||||
defp build_flag_object(act) when is_map(act) or is_binary(act) do
|
||||
id =
|
||||
case act do
|
||||
%Activity{} = act -> act.data["id"]
|
||||
|
@ -634,7 +732,6 @@ defp build_flag_object(%{account: account, statuses: statuses} = _) do
|
|||
"published" => activity.object.data["published"],
|
||||
"actor" => AccountView.render("show.json", %{user: actor})
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_flag_object(_), do: []
|
||||
|
@ -679,6 +776,94 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
|
|||
end
|
||||
|
||||
#### Report-related helpers
|
||||
def get_reports(params, page, page_size) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", "Flag")
|
||||
|> Map.put("skip_preload", true)
|
||||
|> Map.put("total", true)
|
||||
|> Map.put("limit", page_size)
|
||||
|> Map.put("offset", (page - 1) * page_size)
|
||||
|
||||
ActivityPub.fetch_activities([], params, :offset)
|
||||
end
|
||||
|
||||
@spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
|
||||
required(:groups) => [
|
||||
%{
|
||||
required(:date) => String.t(),
|
||||
required(:account) => %{},
|
||||
required(:status) => %{},
|
||||
required(:actors) => [%User{}],
|
||||
required(:reports) => [%Activity{}]
|
||||
}
|
||||
],
|
||||
required(:total) => integer
|
||||
}
|
||||
def get_reports_grouped_by_status(groups) do
|
||||
parsed_groups =
|
||||
groups
|
||||
|> Enum.map(fn entry ->
|
||||
activity =
|
||||
case Jason.decode(entry.activity) do
|
||||
{:ok, activity} -> activity
|
||||
_ -> build_flag_object(entry.activity)
|
||||
end
|
||||
|
||||
parse_report_group(activity)
|
||||
end)
|
||||
|
||||
%{
|
||||
groups: parsed_groups
|
||||
}
|
||||
end
|
||||
|
||||
def parse_report_group(activity) do
|
||||
reports = get_reports_by_status_id(activity["id"])
|
||||
max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
|
||||
actors = Enum.map(reports, & &1.user_actor)
|
||||
|
||||
%{
|
||||
date: max_date.data["published"],
|
||||
account: activity["actor"],
|
||||
status: %{
|
||||
id: activity["id"],
|
||||
content: activity["content"],
|
||||
published: activity["published"]
|
||||
},
|
||||
actors: Enum.uniq(actors),
|
||||
reports: reports
|
||||
}
|
||||
end
|
||||
|
||||
def get_reports_by_status_id(ap_id) do
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}])
|
||||
)
|
||||
|> Activity.with_preloaded_user_actor()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_reported_activities() :: [
|
||||
%{
|
||||
required(:activity) => String.t(),
|
||||
required(:date) => String.t()
|
||||
}
|
||||
]
|
||||
def get_reported_activities do
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
select: %{
|
||||
date: fragment("max(?->>'published') date", a.data),
|
||||
activity:
|
||||
fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
|
||||
},
|
||||
group_by: fragment("activity"),
|
||||
order_by: fragment("date DESC")
|
||||
)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def update_report_state(%Activity{} = activity, state)
|
||||
when state in @strip_status_report_states do
|
||||
|
@ -702,11 +887,29 @@ def update_report_state(%Activity{} = activity, state) when state in @supported_
|
|||
|> Repo.update()
|
||||
end
|
||||
|
||||
def update_report_state(activity_ids, state) when state in @supported_report_states do
|
||||
activities_num = length(activity_ids)
|
||||
|
||||
from(a in Activity, where: a.id in ^activity_ids)
|
||||
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|
||||
|> Repo.update_all([])
|
||||
|> case do
|
||||
{^activities_num, _} -> :ok
|
||||
_ -> {:error, activity_ids}
|
||||
end
|
||||
end
|
||||
|
||||
def update_report_state(_, _), do: {:error, "Unsupported state"}
|
||||
|
||||
def strip_report_status_data(activity) do
|
||||
[actor | reported_activities] = activity.data["object"]
|
||||
stripped_activities = Enum.map(reported_activities, & &1["id"])
|
||||
|
||||
stripped_activities =
|
||||
Enum.map(reported_activities, fn
|
||||
act when is_map(act) -> act["id"]
|
||||
act when is_binary(act) -> act
|
||||
end)
|
||||
|
||||
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
|
||||
|
||||
{:ok, %{activity | data: new_data}}
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
|
||||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
|
||||
def is_public?(%Object{data: data}), do: is_public?(data)
|
||||
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
|
||||
def is_public?(%Activity{data: data}), do: is_public?(data)
|
||||
def is_public?(%{"directMessage" => true}), do: false
|
||||
def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
|
||||
|
|
|
@ -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
|
||||
|
@ -226,6 +227,21 @@ def user_show(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
end
|
||||
|
||||
def list_instance_statuses(conn, %{"instance" => instance} = params) do
|
||||
{page, page_size} = page_params(params)
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_instance_activities(%{
|
||||
"instance" => instance,
|
||||
"limit" => page_size,
|
||||
"offset" => (page - 1) * page_size
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("index.json", %{activities: activities, as: :activity})
|
||||
end
|
||||
|
||||
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
||||
godmode = params["godmode"] == "true" || params["godmode"] == true
|
||||
|
||||
|
@ -334,7 +350,7 @@ def list_users(conn, params) do
|
|||
}
|
||||
|
||||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
|
||||
{:ok, users, count} <- filter_relay_user(users, count),
|
||||
{:ok, users, count} <- filter_service_users(users, count),
|
||||
do:
|
||||
conn
|
||||
|> json(
|
||||
|
@ -346,15 +362,16 @@ def list_users(conn, params) do
|
|||
)
|
||||
end
|
||||
|
||||
defp filter_relay_user(users, count) do
|
||||
filtered_users = Enum.reject(users, &relay_user?/1)
|
||||
count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count
|
||||
defp filter_service_users(users, count) do
|
||||
filtered_users = Enum.reject(users, &service_user?/1)
|
||||
count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
|
||||
|
||||
{:ok, filtered_users, count}
|
||||
end
|
||||
|
||||
defp relay_user?(user) do
|
||||
user.ap_id == Relay.relay_ap_id()
|
||||
defp service_user?(user) do
|
||||
String.match?(user.ap_id, ~r/.*\/relay$/) or
|
||||
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
|
||||
end
|
||||
|
||||
@filters ~w(local external active deactivated is_admin is_moderator)
|
||||
|
@ -607,10 +624,16 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
|
||||
@doc "Force password reset for a given user"
|
||||
def force_password_reset(conn, %{"nickname" => nickname}) do
|
||||
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
|
||||
def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.force_password_reset_async(user)
|
||||
Enum.map(users, &User.force_password_reset_async/1)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "force_password_reset"
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
|
@ -618,19 +641,17 @@ def force_password_reset(conn, %{"nickname" => nickname}) 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
|
||||
|
@ -643,17 +664,26 @@ def report_show(conn, %{"id" => id}) do
|
|||
end
|
||||
end
|
||||
|
||||
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
|
||||
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
|
||||
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: report
|
||||
subject: activity
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("show.json", Report.extract_report_info(report))
|
||||
activity
|
||||
else
|
||||
{:error, message} -> %{id: report["id"], error: message}
|
||||
end
|
||||
end)
|
||||
|
||||
case Enum.any?(result, &Map.has_key?(&1, :error)) do
|
||||
true -> json_response(conn, :bad_request, result)
|
||||
false -> json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -785,6 +815,34 @@ def reload_emoji(conn, _params) do
|
|||
conn |> json("ok")
|
||||
end
|
||||
|
||||
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.toggle_confirmation(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "confirm_email"
|
||||
})
|
||||
|
||||
conn |> json("")
|
||||
end
|
||||
|
||||
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.try_send_confirmation_email(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "resend_confirmation_email"
|
||||
})
|
||||
|
||||
conn |> json("")
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|
|
|
@ -36,7 +36,8 @@ def render("show.json", %{user: user}) do
|
|||
"deactivated" => user.deactivated,
|
||||
"local" => user.local,
|
||||
"roles" => User.roles(user),
|
||||
"tags" => user.tags || []
|
||||
"tags" => user.tags || [],
|
||||
"confirmation_pending" => user.confirmation_pending
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -42,6 +42,26 @@ def render("show.json", %{report: report, user: user, account: account, statuses
|
|||
}
|
||||
end
|
||||
|
||||
def render("index_grouped.json", %{groups: groups}) do
|
||||
reports =
|
||||
Enum.map(groups, fn group ->
|
||||
%{
|
||||
date: group[:date],
|
||||
account: group[:account],
|
||||
status: group[:status],
|
||||
actors: Enum.map(group[:actors], &merge_account_views/1),
|
||||
reports:
|
||||
group[:reports]
|
||||
|> Enum.map(&Report.extract_report_info(&1))
|
||||
|> Enum.map(&render(__MODULE__, "show.json", &1))
|
||||
}
|
||||
end)
|
||||
|
||||
%{
|
||||
reports: reports
|
||||
}
|
||||
end
|
||||
|
||||
defp merge_account_views(%User{} = user) do
|
||||
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|
||||
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||
|
|
|
@ -120,6 +120,25 @@ def unfavorite(id_or_ap_id, user) do
|
|||
end
|
||||
end
|
||||
|
||||
def react_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
object <- Object.normalize(activity) do
|
||||
ActivityPub.react_with_emoji(user, object, emoji)
|
||||
else
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Could not add reaction emoji")}
|
||||
end
|
||||
end
|
||||
|
||||
def unreact_with_emoji(id, user, emoji) do
|
||||
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
|
||||
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
|
||||
else
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Could not remove reaction emoji")}
|
||||
end
|
||||
end
|
||||
|
||||
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
|
||||
with :ok <- validate_not_author(object, user),
|
||||
:ok <- validate_existing_votes(user, object),
|
||||
|
@ -351,6 +370,13 @@ defp get_reported_account(account_id) do
|
|||
end
|
||||
end
|
||||
|
||||
def update_report_state(activity_ids, state) when is_list(activity_ids) do
|
||||
case Utils.update_report_state(activity_ids, state) do
|
||||
:ok -> {:ok, activity_ids}
|
||||
_ -> {:error, dgettext("errors", "Could not update state")}
|
||||
end
|
||||
end
|
||||
|
||||
def update_report_state(activity_id, state) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
Utils.update_report_state(activity, state)
|
||||
|
|
|
@ -451,6 +451,8 @@ def maybe_notify_to_recipients(
|
|||
recipients ++ to
|
||||
end
|
||||
|
||||
def maybe_notify_to_recipients(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_mentioned_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
|
||||
|
@ -502,6 +504,17 @@ def maybe_notify_subscribers(
|
|||
|
||||
def maybe_notify_subscribers(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
|
||||
user
|
||||
|> User.get_followers()
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|> Enum.concat(recipients)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_notify_followers(recipients, _), do: recipients
|
||||
|
||||
def maybe_extract_mentions(%{"tag" => tag}) do
|
||||
tag
|
||||
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|
||||
|
|
|
@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Pleroma.Plugs.HTTPSecurityPlug)
|
||||
plug(Pleroma.Plugs.UploadedMedia)
|
||||
|
||||
@static_cache_control "public, no-cache"
|
||||
@static_cache_control "public max-age=86400 must-revalidate"
|
||||
|
||||
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
|
||||
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
|
||||
|
|
|
@ -33,21 +33,22 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
|
|||
|
||||
def feed(conn, %{"nickname" => nickname} = params) do
|
||||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
|
||||
query_params =
|
||||
params
|
||||
|> Map.take(["max_id"])
|
||||
|> Map.put("type", ["Create"])
|
||||
|> Map.put("whole_db", true)
|
||||
|> Map.put("actor_id", user.ap_id)
|
||||
|
||||
activities =
|
||||
query_params
|
||||
%{
|
||||
"type" => ["Create"],
|
||||
"whole_db" => true,
|
||||
"actor_id" => user.ap_id
|
||||
}
|
||||
|> Map.merge(Map.take(params, ["max_id"]))
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> render("feed.xml", user: user, activities: activities)
|
||||
|> render("feed.xml",
|
||||
user: user,
|
||||
activities: activities,
|
||||
feed_config: Pleroma.Config.get([:feed])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,12 +6,23 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
use Phoenix.HTML
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def prepare_activity(activity) do
|
||||
object = activity_object(activity)
|
||||
|
||||
%{
|
||||
activity: activity,
|
||||
data: Map.get(object, :data),
|
||||
object: object
|
||||
}
|
||||
end
|
||||
|
||||
def most_recent_update(activities, user) do
|
||||
(List.first(activities) || user).updated_at
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
@ -23,31 +34,23 @@ def logo(user) do
|
|||
|> MediaProxy.url()
|
||||
end
|
||||
|
||||
def last_activity(activities) do
|
||||
List.last(activities)
|
||||
def last_activity(activities), do: List.last(activities)
|
||||
|
||||
def activity_object(activity), do: Object.normalize(activity)
|
||||
|
||||
def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
|
||||
content
|
||||
|> Formatter.truncate(opts[:max_length], opts[:omission])
|
||||
|> escape()
|
||||
end
|
||||
|
||||
def activity_object(activity) do
|
||||
Object.normalize(activity)
|
||||
end
|
||||
|
||||
def activity_object_data(activity) do
|
||||
activity
|
||||
|> activity_object()
|
||||
|> Map.get(:data)
|
||||
end
|
||||
|
||||
def activity_content(activity) do
|
||||
content = activity_object_data(activity)["content"]
|
||||
|
||||
def activity_content(%{data: %{"content" => content}}) do
|
||||
content
|
||||
|> String.replace(~r/[\n\r]/, "")
|
||||
|> escape()
|
||||
end
|
||||
|
||||
def activity_context(activity) do
|
||||
activity.data["context"]
|
||||
end
|
||||
def activity_context(activity), do: activity.data["context"]
|
||||
|
||||
def attachment_href(attachment) do
|
||||
attachment["url"]
|
||||
|
|
|
@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
@relations [:follow, :unfollow]
|
||||
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
|
||||
|
||||
plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
|
||||
plug(RateLimiter, :relations_actions when action in @relations)
|
||||
plug(RateLimiter, :app_account_creation when action == :create)
|
||||
plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
|
||||
plug(RateLimiter, [name: :relations_actions] when action in @relations)
|
||||
plug(RateLimiter, [name: :app_account_creation] when action == :create)
|
||||
plug(:assign_account_by_id when action in @needs_account)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
@ -152,6 +152,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|
|||
:hide_favorites,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:allow_following_move,
|
||||
:discoverable
|
||||
]
|
||||
|> Enum.reduce(%{}, fn key, acc ->
|
||||
|
@ -238,7 +239,7 @@ def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
|
|||
@doc "GET /api/v1/accounts/:id"
|
||||
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
|
||||
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
|
||||
true <- User.visible_for?(user, for_user) do
|
||||
render(conn, "show.json", user: user, for: for_user)
|
||||
else
|
||||
_e -> render_error(conn, :not_found, "Can't find user")
|
||||
|
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
|
||||
@local_mastodon_name "Mastodon-Local"
|
||||
|
||||
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
|
||||
plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
|
||||
|
||||
@doc "GET /web/login"
|
||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do
|
||||
|
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
|
|||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
plug(RateLimiter, :search when action in [:search, :search2, :account_search])
|
||||
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
|
||||
|
||||
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
||||
accounts = User.search(query, search_options(params, user))
|
||||
|
|
|
@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
|
||||
plug(
|
||||
RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
|
||||
[name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
|
||||
when action in ~w(reblog unreblog)a
|
||||
)
|
||||
|
||||
plug(
|
||||
RateLimiter,
|
||||
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
|
||||
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
|
||||
when action in ~w(favourite unfavourite)a
|
||||
)
|
||||
|
||||
plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
|
||||
plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
|
|
|
@ -71,7 +71,6 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
|||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities, %{"local" => local_only})
|
||||
|
@ -110,7 +109,6 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do
|
|||
|> Map.put("tag_all", tag_all)
|
||||
|> Map.put("tag_reject", tag_reject)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities, %{"local" => local_only})
|
||||
|
|
|
@ -71,18 +71,17 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
user_info = User.get_cached_user_info(user)
|
||||
|
||||
following_count =
|
||||
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
|
||||
user_info.following_count
|
||||
user.following_count || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
followers_count =
|
||||
if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
|
||||
user_info.follower_count
|
||||
user.follower_count || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
|
@ -144,7 +143,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|
||||
# Pleroma extension
|
||||
pleroma: %{
|
||||
confirmation_pending: user_info.confirmation_pending,
|
||||
confirmation_pending: user.confirmation_pending,
|
||||
tags: user.tags,
|
||||
hide_followers_count: user.hide_followers_count,
|
||||
hide_follows_count: user.hide_follows_count,
|
||||
|
@ -157,12 +156,13 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
}
|
||||
}
|
||||
|> maybe_put_role(user, opts[:for])
|
||||
|> maybe_put_settings(user, opts[:for], user_info)
|
||||
|> maybe_put_settings(user, opts[:for], opts)
|
||||
|> maybe_put_notification_settings(user, opts[:for])
|
||||
|> maybe_put_settings_store(user, opts[:for], opts)
|
||||
|> maybe_put_chat_token(user, opts[:for], opts)
|
||||
|> maybe_put_activation_status(user, opts[:for])
|
||||
|> maybe_put_follow_requests_count(user, opts[:for])
|
||||
|> maybe_put_allow_following_move(user, opts[:for])
|
||||
|> maybe_put_unread_conversation_count(user, opts[:for])
|
||||
end
|
||||
|
||||
|
@ -191,7 +191,7 @@ defp maybe_put_settings(
|
|||
data,
|
||||
%User{id: user_id} = user,
|
||||
%User{id: user_id},
|
||||
_user_info
|
||||
_opts
|
||||
) do
|
||||
data
|
||||
|> Kernel.put_in([:source, :privacy], user.default_scope)
|
||||
|
@ -239,6 +239,12 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id:
|
|||
|
||||
defp maybe_put_notification_settings(data, _, _), do: data
|
||||
|
||||
defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move)
|
||||
end
|
||||
|
||||
defp maybe_put_allow_following_move(data, _, _), do: data
|
||||
|
||||
defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
|
||||
Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)
|
||||
end
|
||||
|
|
|
@ -12,7 +12,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
|
|||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("participations.json", %{participations: participations, for: user}) do
|
||||
render_many(participations, __MODULE__, "participation.json", as: :participation, for: user)
|
||||
safe_render_many(participations, __MODULE__, "participation.json", %{
|
||||
as: :participation,
|
||||
for: user
|
||||
})
|
||||
end
|
||||
|
||||
def render("participation.json", %{participation: participation, for: user}) do
|
||||
|
|
|
@ -37,32 +37,24 @@ def render("show.json", %{
|
|||
}
|
||||
|
||||
case mastodon_type do
|
||||
"mention" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: activity, for: user})
|
||||
})
|
||||
|
||||
"favourite" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"reblog" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
"mention" -> put_status(response, activity, user)
|
||||
"favourite" -> put_status(response, parent_activity, user)
|
||||
"reblog" -> put_status(response, parent_activity, user)
|
||||
"move" -> put_target(response, activity, user)
|
||||
"follow" -> response
|
||||
_ -> nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp put_status(response, activity, user) do
|
||||
Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
|
||||
end
|
||||
|
||||
defp put_target(response, activity, user) do
|
||||
target = User.get_cached_by_ap_id(activity.data["target"])
|
||||
Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,8 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
@ -245,12 +243,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
direct_conversation_id =
|
||||
with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
|
||||
{_, true} <- {:include_id, opts[:with_direct_conversation_id]},
|
||||
{_, %User{} = for_user} <- {:for_user, opts[:for]},
|
||||
%{data: %{"context" => context}} when is_binary(context) <- activity,
|
||||
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
|
||||
%Participation{id: participation_id} <-
|
||||
Participation.for_user_and_conversation(for_user, conversation) do
|
||||
participation_id
|
||||
{_, %User{} = for_user} <- {:for_user, opts[:for]} do
|
||||
Activity.direct_conversation_id(activity, for_user)
|
||||
else
|
||||
{:direct_conversation_id, participation_id} when is_integer(participation_id) ->
|
||||
participation_id
|
||||
|
|
|
@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
|
|||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
plug(RateLimiter, :authentication when action in [:user_exists, :check_password])
|
||||
plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password)
|
||||
plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
|
||||
plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
|
||||
|
||||
def user_exists(conn, %{"user" => username}) do
|
||||
with %User{} <- Repo.get_by(User, nickname: username, local: true) do
|
||||
|
|
|
@ -46,6 +46,7 @@ def raw_nodeinfo do
|
|||
|
||||
data
|
||||
|> Map.merge(%{quarantined_instances: quarantined})
|
||||
|> Map.put(:enabled, Config.get([:instance, :federating]))
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
@ -58,6 +59,7 @@ def raw_nodeinfo do
|
|||
"polls",
|
||||
"pleroma_explicit_addressing",
|
||||
"shareable_emoji_packs",
|
||||
"multifetch",
|
||||
if Config.get([:media_proxy, :enabled]) do
|
||||
"media_proxy"
|
||||
end,
|
||||
|
@ -118,6 +120,12 @@ def raw_nodeinfo do
|
|||
banner: Config.get([:instance, :banner_upload_limit]),
|
||||
background: Config.get([:instance, :background_upload_limit])
|
||||
},
|
||||
fieldsLimits: %{
|
||||
maxFields: Config.get([:instance, :max_account_fields]),
|
||||
maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
|
||||
nameLength: Config.get([:instance, :account_field_name_length]),
|
||||
valueLength: Config.get([:instance, :account_field_value_length])
|
||||
},
|
||||
accountActivationRequired: Config.get([:instance, :account_activation_required], false),
|
||||
invitesEnabled: Config.get([:instance, :invites_enabled], false),
|
||||
mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Helpers.UriHelper
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_flash)
|
||||
plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
|
||||
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
|
||||
|
||||
action_fallback(Pleroma.Web.OAuth.FallbackController)
|
||||
|
||||
|
@ -36,7 +37,7 @@ def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
|
|||
authorize(conn, Map.merge(params, auth_attrs))
|
||||
end
|
||||
|
||||
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
|
||||
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" => _} = params) do
|
||||
if ControllerHelper.truthy_param?(params["force_login"]) do
|
||||
do_authorize(conn, params)
|
||||
else
|
||||
|
@ -44,6 +45,22 @@ def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
|
|||
end
|
||||
end
|
||||
|
||||
# Note: the token is set in oauth_plug, but the token and client do not always go together.
|
||||
# For example, MastodonFE's token is set if user requests with another client,
|
||||
# after user already authorized to MastodonFE.
|
||||
# So we have to check client and token.
|
||||
def authorize(
|
||||
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
|
||||
%{"client_id" => client_id} = params
|
||||
) do
|
||||
with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
|
||||
^client_id <- t.app.client_id do
|
||||
handle_existing_authorization(conn, params)
|
||||
else
|
||||
_ -> do_authorize(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
|
||||
|
||||
defp do_authorize(%Plug.Conn{} = conn, params) do
|
||||
|
|
|
@ -8,17 +8,17 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
alias Fallback.RedirectController
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPubController
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Metadata.PlayerView
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
|
||||
RateLimiter,
|
||||
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
@ -37,11 +37,9 @@ def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
|
|||
with id <- o_status_url(conn, :object, uuid),
|
||||
{_, %Activity{} = activity} <-
|
||||
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)},
|
||||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
||||
case format do
|
||||
"html" -> redirect(conn, to: "/notice/#{activity.id}")
|
||||
_ -> represent_activity(conn, nil, activity, user)
|
||||
_ -> redirect(conn, to: "/notice/#{activity.id}")
|
||||
end
|
||||
else
|
||||
reason when reason in [{:public?, false}, {:activity, nil}] ->
|
||||
|
@ -60,11 +58,9 @@ def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
|
|||
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
|
||||
with id <- o_status_url(conn, :activity, uuid),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)},
|
||||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
||||
case format do
|
||||
"html" -> redirect(conn, to: "/notice/#{activity.id}")
|
||||
_ -> represent_activity(conn, format, activity, user)
|
||||
_ -> redirect(conn, to: "/notice/#{activity.id}")
|
||||
end
|
||||
else
|
||||
reason when reason in [{:public?, false}, {:activity, nil}] ->
|
||||
|
@ -80,7 +76,15 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
|
|||
{_, true} <- {:public?, Visibility.is_public?(activity)},
|
||||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
cond do
|
||||
format == "html" && activity.data["type"] == "Create" ->
|
||||
format in ["json", "activity+json"] ->
|
||||
if activity.local do
|
||||
%{data: %{"id" => redirect_url}} = Object.normalize(activity)
|
||||
redirect(conn, external: redirect_url)
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
activity.data["type"] == "Create" ->
|
||||
%Object{} = object = Object.normalize(activity)
|
||||
|
||||
RedirectController.redirector_with_meta(
|
||||
|
@ -93,11 +97,8 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
|
|||
}
|
||||
)
|
||||
|
||||
format == "html" ->
|
||||
RedirectController.redirector(conn, nil)
|
||||
|
||||
true ->
|
||||
represent_activity(conn, format, activity, user)
|
||||
RedirectController.redirector(conn, nil)
|
||||
end
|
||||
else
|
||||
reason when reason in [{:public?, false}, {:activity, nil}] ->
|
||||
|
@ -134,24 +135,6 @@ def notice_player(conn, %{"id" => id}) do
|
|||
end
|
||||
end
|
||||
|
||||
defp represent_activity(
|
||||
conn,
|
||||
"activity+json",
|
||||
%Activity{data: %{"type" => "Create"}} = activity,
|
||||
_user
|
||||
) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> put_view(ObjectView)
|
||||
|> render("object.json", %{object: object})
|
||||
end
|
||||
|
||||
defp represent_activity(_conn, _, _, _) do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
render_error(conn, :not_found, "Not found")
|
||||
end
|
||||
|
|
|
@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
|||
when action != :confirmation_resend
|
||||
)
|
||||
|
||||
plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
|
||||
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
|
||||
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
|
||||
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
|
||||
|
||||
|
|
|
@ -7,10 +7,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
|||
|
||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.ConversationView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
@ -29,6 +34,47 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
|||
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
|
||||
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
%Object{data: %{"reactions" => emoji_reactions}} <- Object.normalize(activity) do
|
||||
reactions =
|
||||
emoji_reactions
|
||||
|> Enum.map(fn {emoji, users} ->
|
||||
users = Enum.map(users, &User.get_cached_by_ap_id/1)
|
||||
{emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
conn
|
||||
|> json(reactions)
|
||||
else
|
||||
_e ->
|
||||
conn
|
||||
|> json(%{})
|
||||
end
|
||||
end
|
||||
|
||||
def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
|
||||
with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
|
||||
activity <- Activity.get_by_id(activity_id) do
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("show.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
|
||||
"id" => activity_id,
|
||||
"emoji" => emoji
|
||||
}) do
|
||||
with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
|
||||
activity <- Activity.get_by_id(activity_id) do
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("show.json", %{activity: activity, for: user, as: :activity})
|
||||
end
|
||||
end
|
||||
|
||||
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
|
||||
with %Participation{} = participation <- Participation.get(participation_id),
|
||||
true <- user.id == participation.user_id do
|
||||
|
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.Push.Impl do
|
|||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like"]
|
||||
@types ["Create", "Follow", "Announce", "Like", "Move"]
|
||||
|
||||
@doc "Performs sending notifications for user subscriptions"
|
||||
@spec perform(Notification.t()) :: list(any) | :error
|
||||
|
@ -33,6 +33,8 @@ def perform(
|
|||
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
|
||||
avatar_url = User.avatar_url(actor)
|
||||
object = Object.normalize(activity)
|
||||
user = User.get_cached_by_id(user_id)
|
||||
direct_conversation_id = Activity.direct_conversation_id(activity, user)
|
||||
|
||||
for subscription <- fetch_subsriptions(user_id),
|
||||
get_in(subscription.data, ["alerts", type]) do
|
||||
|
@ -45,7 +47,8 @@ def perform(
|
|||
icon: avatar_url,
|
||||
preferred_locale: "en",
|
||||
pleroma: %{
|
||||
activity_id: activity_id
|
||||
activity_id: activity_id,
|
||||
direct_conversation_id: direct_conversation_id
|
||||
}
|
||||
}
|
||||
|> Jason.encode!()
|
||||
|
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.Router do
|
|||
pipeline :oauth do
|
||||
plug(:fetch_session)
|
||||
plug(Pleroma.Plugs.OAuthPlug)
|
||||
plug(Pleroma.Plugs.UserEnabledPlug)
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
|
@ -171,15 +172,21 @@ defmodule Pleroma.Web.Router do
|
|||
post("/users/email_invite", AdminAPIController, :email_invite)
|
||||
|
||||
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
|
||||
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
|
||||
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
|
||||
|
||||
get("/users", AdminAPIController, :list_users)
|
||||
get("/users/:nickname", AdminAPIController, :user_show)
|
||||
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
|
||||
|
||||
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
||||
|
||||
patch("/users/confirm_email", AdminAPIController, :confirm_email)
|
||||
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
|
||||
|
||||
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)
|
||||
|
@ -260,6 +267,12 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
end
|
||||
|
||||
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
||||
pipe_through(:api)
|
||||
|
||||
get("/statuses/:id/emoji_reactions_by", PleromaAPIController, :emoji_reactions_by)
|
||||
end
|
||||
|
||||
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
||||
scope [] do
|
||||
pipe_through(:authenticated_api)
|
||||
|
@ -273,6 +286,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)
|
||||
|
@ -495,6 +510,7 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
pipeline :ostatus do
|
||||
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
|
||||
plug(Pleroma.Plugs.StaticFEPlug)
|
||||
end
|
||||
|
||||
pipeline :oembed do
|
||||
|
|
163
lib/pleroma/web/static_fe/static_fe_controller.ex
Normal file
163
lib/pleroma/web/static_fe/static_fe_controller.ex
Normal file
|
@ -0,0 +1,163 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.StaticFE.StaticFEController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Metadata
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
plug(:put_layout, :static_fe)
|
||||
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
|
||||
plug(:assign_id)
|
||||
|
||||
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
||||
|
||||
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
|
||||
do: name
|
||||
|
||||
defp get_title(%Object{data: %{"summary" => summary}}) when is_binary(summary),
|
||||
do: summary
|
||||
|
||||
defp get_title(_), do: nil
|
||||
|
||||
defp not_found(conn, message) do
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> render("error.html", %{message: message, meta: ""})
|
||||
end
|
||||
|
||||
def get_counts(%Activity{} = activity) do
|
||||
%Object{data: data} = Object.normalize(activity)
|
||||
|
||||
%{
|
||||
likes: data["like_count"] || 0,
|
||||
replies: data["repliesCount"] || 0,
|
||||
announces: data["announcement_count"] || 0
|
||||
}
|
||||
end
|
||||
|
||||
def represent(%Activity{} = activity), do: represent(activity, false)
|
||||
|
||||
def represent(%Activity{object: %Object{data: data}} = activity, selected) do
|
||||
{:ok, user} = User.get_or_fetch(activity.object.data["actor"])
|
||||
|
||||
link =
|
||||
case user.local do
|
||||
true -> Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
|
||||
_ -> data["url"] || data["external_url"] || data["id"]
|
||||
end
|
||||
|
||||
%{
|
||||
user: user,
|
||||
title: get_title(activity.object),
|
||||
content: data["content"] || nil,
|
||||
attachment: data["attachment"],
|
||||
link: link,
|
||||
published: data["published"],
|
||||
sensitive: data["sensitive"],
|
||||
selected: selected,
|
||||
counts: get_counts(activity),
|
||||
id: activity.id
|
||||
}
|
||||
end
|
||||
|
||||
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
|
||||
with %Activity{local: true} = activity <-
|
||||
Activity.get_by_id_with_object(notice_id),
|
||||
true <- Visibility.is_public?(activity.object),
|
||||
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
|
||||
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
|
||||
|
||||
timeline =
|
||||
activity.object.data["context"]
|
||||
|> ActivityPub.fetch_activities_for_context(%{})
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
|
||||
|
||||
render(conn, "conversation.html", %{activities: timeline, meta: meta})
|
||||
else
|
||||
%Activity{object: %Object{data: data}} ->
|
||||
conn
|
||||
|> put_status(:found)
|
||||
|> redirect(external: data["url"] || data["external_url"] || data["id"])
|
||||
|
||||
_ ->
|
||||
not_found(conn, "Post not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
|
||||
case User.get_cached_by_nickname_or_id(username_or_id) do
|
||||
%User{} = user ->
|
||||
meta = Metadata.build_tags(%{user: user})
|
||||
|
||||
timeline =
|
||||
ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys))
|
||||
|> Enum.map(&represent/1)
|
||||
|
||||
prev_page_id =
|
||||
(params["min_id"] || params["max_id"]) &&
|
||||
List.first(timeline) && List.first(timeline).id
|
||||
|
||||
next_page_id = List.last(timeline) && List.last(timeline).id
|
||||
|
||||
render(conn, "profile.html", %{
|
||||
user: user,
|
||||
timeline: timeline,
|
||||
prev_page_id: prev_page_id,
|
||||
next_page_id: next_page_id,
|
||||
meta: meta
|
||||
})
|
||||
|
||||
_ ->
|
||||
not_found(conn, "User not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{object_id: _}} = conn, _params) do
|
||||
url = Helpers.url(conn) <> conn.request_path
|
||||
|
||||
case Activity.get_create_by_object_ap_id_with_object(url) do
|
||||
%Activity{} = activity ->
|
||||
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
|
||||
redirect(conn, to: to)
|
||||
|
||||
_ ->
|
||||
not_found(conn, "Post not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{activity_id: _}} = conn, _params) do
|
||||
url = Helpers.url(conn) <> conn.request_path
|
||||
|
||||
case Activity.get_by_ap_id(url) do
|
||||
%Activity{} = activity ->
|
||||
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
|
||||
redirect(conn, to: to)
|
||||
|
||||
_ ->
|
||||
not_found(conn, "Post not found.")
|
||||
end
|
||||
end
|
||||
|
||||
def assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
|
||||
do: assign(conn, :notice_id, notice_id)
|
||||
|
||||
def assign_id(%{path_info: ["users", user_id]} = conn, _opts),
|
||||
do: assign(conn, :username_or_id, user_id)
|
||||
|
||||
def assign_id(%{path_info: ["objects", object_id]} = conn, _opts),
|
||||
do: assign(conn, :object_id, object_id)
|
||||
|
||||
def assign_id(%{path_info: ["activities", activity_id]} = conn, _opts),
|
||||
do: assign(conn, :activity_id, activity_id)
|
||||
|
||||
def assign_id(conn, _opts), do: conn
|
||||
end
|
47
lib/pleroma/web/static_fe/static_fe_view.ex
Normal file
47
lib/pleroma/web/static_fe/static_fe_view.ex
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.StaticFE.StaticFEView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Calendar.Strftime
|
||||
alias Pleroma.Emoji.Formatter
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Gettext
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
use Phoenix.HTML
|
||||
|
||||
@media_types ["image", "audio", "video"]
|
||||
|
||||
def emoji_for_user(%User{} = user) do
|
||||
user.source_data
|
||||
|> Map.get("tag", [])
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|
||||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
{String.trim(name, ":"), url}
|
||||
end)
|
||||
end
|
||||
|
||||
def fetch_media_type(%{"mediaType" => mediaType}) do
|
||||
Utils.fetch_media_type(@media_types, mediaType)
|
||||
end
|
||||
|
||||
def format_date(date) do
|
||||
{:ok, date, _} = DateTime.from_iso8601(date)
|
||||
Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC")
|
||||
end
|
||||
|
||||
def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma")
|
||||
|
||||
def open_content? do
|
||||
Pleroma.Config.get(
|
||||
[:frontend_configurations, :collapse_message_with_subjects],
|
||||
true
|
||||
)
|
||||
end
|
||||
end
|
|
@ -2,11 +2,13 @@
|
|||
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<id><%= @data["id"] %></id>
|
||||
<title><%= "New note by #{@user.nickname}" %></title>
|
||||
<content type="html"><%= activity_content(@activity) %></content>
|
||||
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
|
||||
<content type="html"><%= activity_content(@object) %></content>
|
||||
<published><%= @data["published"] %></published>
|
||||
<updated><%= @data["published"] %></updated>
|
||||
<ostatus:conversation ref="<%= activity_context(@activity) %>"><%= activity_context(@activity) %></ostatus:conversation>
|
||||
<ostatus:conversation ref="<%= activity_context(@activity) %>">
|
||||
<%= activity_context(@activity) %>
|
||||
</ostatus:conversation>
|
||||
<link ref="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
|
||||
|
||||
<%= if @data["summary"] do %>
|
||||
|
|
|
@ -19,6 +19,6 @@
|
|||
<% end %>
|
||||
|
||||
<%= for activity <- @activities do %>
|
||||
<%= render @view_module, "_activity.xml", Map.merge(assigns, %{activity: activity, data: activity_object_data(activity)}) %>
|
||||
<%= render @view_module, "_activity.xml", Map.merge(assigns, prepare_activity(activity)) %>
|
||||
<% end %>
|
||||
</feed>
|
||||
|
|
15
lib/pleroma/web/templates/layout/static_fe.html.eex
Normal file
15
lib/pleroma/web/templates/layout/static_fe.html.eex
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
||||
<link rel="stylesheet" href="/static/static-fe.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<%= case @mediaType do %>
|
||||
<% "audio" -> %>
|
||||
<audio src="<%= @url %>" controls="controls"></audio>
|
||||
<% "video" -> %>
|
||||
<video src="<%= @url %>" controls="controls"></video>
|
||||
<% _ -> %>
|
||||
<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
|
||||
<% end %>
|
|
@ -0,0 +1,37 @@
|
|||
<div class="activity" <%= if @selected do %> id="selected" <% end %>>
|
||||
<p class="pull-right">
|
||||
<%= link format_date(@published), to: @link, class: "activity-link" %>
|
||||
</p>
|
||||
<%= render("_user_card.html", %{user: @user}) %>
|
||||
<div class="activity-content">
|
||||
<%= if @title != "" do %>
|
||||
<details <%= if open_content?() do %>open<% end %>>
|
||||
<summary><%= raw @title %></summary>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
</details>
|
||||
<% else %>
|
||||
<div class="e-content"><%= raw @content %></div>
|
||||
<% end %>
|
||||
<%= for %{"name" => name, "url" => [url | _]} <- @attachment do %>
|
||||
<%= if @sensitive do %>
|
||||
<details class="nsfw">
|
||||
<summary><%= Gettext.gettext("sensitive media") %></summary>
|
||||
<div>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
</div>
|
||||
</details>
|
||||
<% else %>
|
||||
<%= render("_attachment.html", %{name: name, url: url["href"],
|
||||
mediaType: fetch_media_type(url)}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if @selected do %>
|
||||
<dl class="counts">
|
||||
<dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd>
|
||||
<dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd>
|
||||
<dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
<div class="p-author h-card">
|
||||
<a class="u-url" rel="author noopener" href="<%= User.profile_url(@user) %>">
|
||||
<div class="avatar">
|
||||
<img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
|
||||
</div>
|
||||
<span class="display-name">
|
||||
<bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
|
||||
<span class="nickname"><%= @user.nickname %></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="conversation">
|
||||
<%= for activity <- @activities do %>
|
||||
<%= render("_notice.html", activity) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</main>
|
|
@ -0,0 +1,7 @@
|
|||
<header>
|
||||
<h1><%= gettext("Oops") %></h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<p><%= @message %></p>
|
||||
</main>
|
|
@ -0,0 +1,31 @@
|
|||
<header>
|
||||
<h1><%= link instance_name(), to: "/" %></h1>
|
||||
|
||||
<h3>
|
||||
<form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
|
||||
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button type="submit" class="collapse">Remote follow</button>
|
||||
</form>
|
||||
<%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
|
||||
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: User.profile_url(@user) %>
|
||||
</h3>
|
||||
<p><%= raw @user.bio %></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="activity-stream">
|
||||
<%= for activity <- @timeline do %>
|
||||
<%= render("_notice.html", Map.put(activity, :selected, false)) %>
|
||||
<% end %>
|
||||
<p id="pagination">
|
||||
<%= if @prev_page_id do %>
|
||||
<%= link "«", to: "?min_id=" <> @prev_page_id %>
|
||||
<% end %>
|
||||
<%= if @prev_page_id && @next_page_id, do: " | " %>
|
||||
<%= if @next_page_id do %>
|
||||
<%= link "»", to: "?max_id=" <> @next_page_id %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
|
@ -71,4 +71,11 @@ def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id},
|
|||
activity = Activity.get_by_id(activity_id)
|
||||
Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
|
||||
end
|
||||
|
||||
def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do
|
||||
origin = User.get_cached_by_id(origin_id)
|
||||
target = User.get_cached_by_id(target_id)
|
||||
|
||||
Pleroma.FollowingRelationship.move_following(origin, target)
|
||||
end
|
||||
end
|
||||
|
|
33
mix.exs
33
mix.exs
|
@ -63,7 +63,7 @@ def copy_nginx_config(%{path: target_path} = release) do
|
|||
def application do
|
||||
[
|
||||
mod: {Pleroma.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :myhtmlex, :swarm],
|
||||
extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :swarm],
|
||||
included_applications: [:ex_syslogger]
|
||||
]
|
||||
end
|
||||
|
@ -102,16 +102,14 @@ defp deps do
|
|||
{:phoenix_ecto, "~> 4.0"},
|
||||
{:ecto_sql, "~> 3.2"},
|
||||
{:postgrex, ">= 0.13.5"},
|
||||
{:oban, "~> 0.8.1"},
|
||||
{:oban, "~> 0.12.0"},
|
||||
{:quantum, "~> 2.3"},
|
||||
{:gettext, "~> 0.15"},
|
||||
{:comeonin, "~> 4.1.1"},
|
||||
{:pbkdf2_elixir, "~> 0.12.3"},
|
||||
{:trailing_format_plug, "~> 0.0.7"},
|
||||
{:fast_sanitize,
|
||||
git: "https://git.pleroma.social/pleroma/fast_sanitize.git",
|
||||
ref: "1af67547a02a104e26c99d03012383e8643bc4c2"},
|
||||
{:html_entities, "~> 0.4"},
|
||||
{:fast_sanitize, "~> 0.1"},
|
||||
{:html_entities, "~> 0.5", override: true},
|
||||
{:phoenix_html, "~> 2.10"},
|
||||
{:calendar, "~> 0.17.4"},
|
||||
{:cachex, "~> 3.0.2"},
|
||||
|
@ -157,7 +155,6 @@ defp deps do
|
|||
{:joken, "~> 2.0"},
|
||||
{:benchee, "~> 1.0"},
|
||||
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
|
||||
{:ex_rated, "~> 1.3"},
|
||||
{:ex_const, "~> 0.2"},
|
||||
{:plug_static_index_html, "~> 1.0.0"},
|
||||
{:excoveralls, "~> 0.11.1", only: :test},
|
||||
|
@ -197,25 +194,19 @@ defp version(version) do
|
|||
identifier_filter = ~r/[^0-9a-z\-]+/i
|
||||
|
||||
# Pre-release version, denoted from patch version with a hyphen
|
||||
{git_tag, git_pre_release} =
|
||||
git_pre_release =
|
||||
with {tag, 0} <-
|
||||
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
|
||||
tag = String.trim(tag),
|
||||
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
|
||||
describe = String.trim(describe),
|
||||
ahead <- String.replace(describe, tag, ""),
|
||||
ahead <- String.trim_leading(ahead, "-") do
|
||||
{String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
|
||||
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
|
||||
describe
|
||||
|> String.trim()
|
||||
|> String.replace(String.trim(tag), "")
|
||||
|> String.trim_leading("-")
|
||||
|> String.trim()
|
||||
else
|
||||
_ ->
|
||||
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
|
||||
{nil, "0-g" <> String.trim(commit_hash)}
|
||||
end
|
||||
|
||||
if git_tag && version != git_tag do
|
||||
Mix.shell().error(
|
||||
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
|
||||
)
|
||||
"0-g" <> String.trim(commit_hash)
|
||||
end
|
||||
|
||||
# Branch name as pre-release version component, denoted with a dot
|
||||
|
|
16
mix.lock
16
mix.lock
|
@ -23,8 +23,8 @@
|
|||
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
|
||||
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
|
||||
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
|
||||
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
|
||||
|
@ -33,18 +33,18 @@
|
|||
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
|
||||
"excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fast_sanitize": {:git, "https://git.pleroma.social/pleroma/fast_sanitize.git", "1af67547a02a104e26c99d03012383e8643bc4c2", [ref: "1af67547a02a104e26c99d03012383e8643bc4c2"]},
|
||||
"fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"},
|
||||
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
|
||||
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm"},
|
||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.0", "40f5c5b9cbe23073b48a4e69c67b6c11974f623a76165e2b92d098c0e88ccb1d", [:mix], [], "hexpm"},
|
||||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
|
||||
"httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
@ -67,7 +67,7 @@
|
|||
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
|
||||
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
|
||||
"oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
|
||||
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"},
|
||||
"phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
@ -97,7 +97,7 @@
|
|||
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
|
||||
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
|
||||
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
|
||||
"tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddMoveSupportToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add(:also_known_as, {:array, :string}, default: [], null: false)
|
||||
add(:allow_following_move, :boolean, default: true, null: false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Pleroma.Repo.Migrations.SetVisibleServiceActors do
|
||||
use Ecto.Migration
|
||||
import Ecto.Query
|
||||
alias Pleroma.Repo
|
||||
|
||||
def up do
|
||||
user_nicknames = ["relay", "internal.fetch"]
|
||||
|
||||
from(
|
||||
u in "users",
|
||||
where: u.nickname in ^user_nicknames,
|
||||
update: [
|
||||
set: [invisible: true]
|
||||
]
|
||||
)
|
||||
|> Repo.update_all([])
|
||||
end
|
||||
|
||||
def down do
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.RemoveInfoFromUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
remove(:info, :map, default: %{})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
defmodule Pleroma.Repo.Migrations.FixMissingFollowingCount do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
"""
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
following_count = sub.count
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
users.id AS sub_id
|
||||
,COUNT (following_relationships.id)
|
||||
FROM
|
||||
following_relationships
|
||||
,users
|
||||
WHERE
|
||||
users.id = following_relationships.follower_id
|
||||
AND following_relationships.state = 'accept'
|
||||
GROUP BY
|
||||
users.id
|
||||
) AS sub
|
||||
WHERE
|
||||
users.id = sub.sub_id
|
||||
AND users.local = TRUE
|
||||
;
|
||||
"""
|
||||
|> execute()
|
||||
|
||||
"""
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
following_count = 0
|
||||
WHERE
|
||||
following_count IS NULL
|
||||
"""
|
||||
|> execute()
|
||||
|
||||
execute("ALTER TABLE users
|
||||
ALTER COLUMN following_count SET DEFAULT 0,
|
||||
ALTER COLUMN following_count SET NOT NULL
|
||||
")
|
||||
end
|
||||
|
||||
def down do
|
||||
execute("ALTER TABLE users
|
||||
ALTER COLUMN following_count DROP DEFAULT,
|
||||
ALTER COLUMN following_count DROP NOT NULL
|
||||
")
|
||||
end
|
||||
end
|
|
@ -1 +1 @@
|
|||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.4e8e80a2f95232cff399.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.24e6ba2d196f6210feda.js></script><script type=text/javascript src=/static/js/app.4ab7097a5650339b9e3d.js></script></body></html>
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.76db8e4cdf29decd5cab.js></script><script type=text/javascript src=/static/js/app.d20ca27d22d74eb7bce0.js></script></body></html>
|
|
@ -28,6 +28,11 @@
|
|||
"oauthRegistrationEndpoint": {
|
||||
"@id": "litepub:oauthRegistrationEndpoint",
|
||||
"@type": "@id"
|
||||
},
|
||||
"EmojiReaction": "litepub:EmojiReaction",
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -99,4 +99,4 @@ .with-subscription-loading .error {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=app.4e8e80a2f95232cff399.css.map*/
|
||||
/*# sourceMappingURL=app.fd71461124f3eb029b1b.css.map*/
|
|
@ -1 +1 @@
|
|||
{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.4e8e80a2f95232cff399.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
|
||||
{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.fd71461124f3eb029b1b.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue