Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop

This commit is contained in:
sadposter 2019-11-13 00:10:01 +00:00
commit 2de30812ea
49 changed files with 1919 additions and 980 deletions

View file

@ -34,7 +34,7 @@ benchmark:
variables: variables:
MIX_ENV: benchmark MIX_ENV: benchmark
services: services:
- name: lainsoykaf/postgres-with-rum - name: postgres:9.6
alias: postgres alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script: script:
@ -46,7 +46,7 @@ benchmark:
unit-testing: unit-testing:
stage: test stage: test
services: services:
- name: lainsoykaf/postgres-with-rum - name: postgres:9.6
alias: postgres alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script: script:
@ -58,7 +58,7 @@ unit-testing:
unit-testing-rum: unit-testing-rum:
stage: test stage: test
services: services:
- name: lainsoykaf/postgres-with-rum - name: minibikini/postgres-with-rum:12
alias: postgres alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
variables: variables:
@ -113,6 +113,7 @@ review_app:
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
- (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true - (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 -- 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: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 - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
@ -138,7 +139,7 @@ stop_review_app:
- ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db - ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db
amd64: amd64:
stage: release stage: release
# TODO: Replace with upstream image when 1.9.0 comes out # TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0 image: rinpatch/elixir:1.9.0-rc.0
only: &release-only only: &release-only

View file

@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **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) - 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 - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Enabled `:instance, extended_nickname_format` in the default config - Enabled `:instance, extended_nickname_format` in the default config
@ -37,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### Added
- Refreshing poll results for remote polls - Refreshing poll results for remote polls
- Authentication: Added rate limit for password-authorized actions / login existence checks - 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 re-count statuses for all users (`mix pleroma.count_statuses`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
<details> <details>
@ -58,10 +60,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers - Mastodon API: Add `/api/v1/markers` for managing timeline read markers
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations` - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
- Configuration: `feed` option for user atom feed.
</details> </details>
### Fixed ### Fixed
- Report emails now include functional links to profiles of remote user accounts - 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> <details>
<summary>API Changes</summary> <summary>API Changes</summary>

View file

@ -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/backend/clients/)
- [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>.
## Installation ## 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 ### 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 ### Docker
While we dont 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>. While we dont 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 ## Community Channels
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixirs install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf). * IRC: **#pleroma** and **#pleroma-dev** on freenode, webchat is available at <https://irc.pleroma.social>
* Build-essential tools * Matrix: <https://matrix.to/#/#freenode_#pleroma:matrix.org> and <https://matrix.to/#/#freenode_#pleroma-dev:matrix.org>
### 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 repositorys 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.

View file

@ -90,7 +90,7 @@
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe], filters: [Pleroma.Upload.Filter.Dedupe],
link_name: true, link_name: false,
proxy_remote: false, proxy_remote: false,
proxy_opts: [ proxy_opts: [
redirect_on_failure: false, redirect_on_failure: false,
@ -257,7 +257,7 @@
mrf_transparency_exclusions: [], mrf_transparency_exclusions: [],
autofollowed_nicknames: [], autofollowed_nicknames: [],
max_pinned_statuses: 1, max_pinned_statuses: 1,
no_attachment_links: false, no_attachment_links: true,
welcome_user_nickname: nil, welcome_user_nickname: nil,
welcome_message: nil, welcome_message: nil,
max_report_comment_size: 1000, max_report_comment_size: 1000,
@ -274,7 +274,13 @@
account_field_name_length: 512, account_field_name_length: 512,
account_field_value_length: 2048, account_field_value_length: 2048,
external_user_synchronization: true, external_user_synchronization: true,
extended_nickname_format: false extended_nickname_format: true
config :pleroma, :feed,
post_title: %{
max_length: 100,
omission: "..."
}
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
@ -599,6 +605,8 @@
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, :static_fe, enabled: false
config :pleroma, :web_cache_ttl, config :pleroma, :web_cache_ttl,
activity_pub: nil, activity_pub: nil,
activity_pub_question: 30_000 activity_pub_question: 30_000

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,10 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot
## Setup ## Setup
### Configuring PostgreSQL ### Configuring PostgreSQL
#### (Optional) Installing RUM indexes #### (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). 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): Debian/Ubuntu (available only on Buster/19.04):
@ -74,7 +78,7 @@ rc-service postgresql restart
# Create the Pleroma user # Create the Pleroma user
adduser --system --shell /bin/false --home /opt/pleroma pleroma adduser --system --shell /bin/false --home /opt/pleroma pleroma
# Set the flavour environment variable to the string you got in Detecting flavour section. # Set the flavour environment variable to the string you got in Detecting flavour section.
# For example if the flavour is `arm64-musl` the command will be # For example if the flavour is `arm64-musl` the command will be
export FLAVOUR="arm64-musl" export FLAVOUR="arm64-musl"
@ -180,7 +184,7 @@ rc-service pleroma start
rc-update add pleroma rc-update add pleroma
``` ```
If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors. If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://webchat.freenode.net/?channels=%23pleroma) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new) Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://webchat.freenode.net/?channels=%23pleroma) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new)

View file

@ -45,7 +45,7 @@ def run(["migrate_from_db", env, delete?]) do
if Pleroma.Config.get([:instance, :dynamic_configuration]) do if Pleroma.Config.get([:instance, :dynamic_configuration]) do
config_path = "config/#{env}.exported_from_db.secret.exs" 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") IO.write(file, "use Mix.Config\r\n")
Repo.all(Config) Repo.all(Config)

View file

@ -36,7 +36,8 @@ def start(_type, _args) do
Pleroma.Emoji, Pleroma.Emoji,
Pleroma.Captcha, Pleroma.Captcha,
Pleroma.Daemons.ScheduledActivityDaemon, Pleroma.Daemons.ScheduledActivityDaemon,
Pleroma.Daemons.ActivityExpirationDaemon Pleroma.Daemons.ActivityExpirationDaemon,
Pleroma.Plugs.RateLimiter.Supervisor
] ++ ] ++
cachex_children() ++ cachex_children() ++
hackney_pool_children() ++ hackney_pool_children() ++

View file

@ -5,7 +5,7 @@ defmodule Pleroma.Docs.JSON do
def process(descriptions) do def process(descriptions) do
config_path = "docs/generate_config.json" 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), json <- generate_json(descriptions),
:ok <- IO.write(file, json), :ok <- IO.write(file, json),
:ok <- File.close(file) do :ok <- File.close(file) do

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -568,7 +568,6 @@ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|> fetch_activities_query(opts) |> fetch_activities_query(opts)
|> restrict_unlisted() |> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination) |> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end end
@valid_visibilities ~w[direct unlisted public private] @valid_visibilities ~w[direct unlisted public private]

View file

@ -33,21 +33,22 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
def feed(conn, %{"nickname" => nickname} = params) do def feed(conn, %{"nickname" => nickname} = params) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} 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 = activities =
query_params %{
"type" => ["Create"],
"whole_db" => true,
"actor_id" => user.ap_id
}
|> Map.merge(Map.take(params, ["max_id"]))
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn conn
|> put_resp_content_type("application/atom+xml") |> 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
end end

View file

@ -6,12 +6,23 @@ defmodule Pleroma.Web.Feed.FeedView do
use Phoenix.HTML use Phoenix.HTML
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
require Pleroma.Constants 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 def most_recent_update(activities, user) do
(List.first(activities) || user).updated_at (List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601() |> NaiveDateTime.to_iso8601()
@ -23,31 +34,23 @@ def logo(user) do
|> MediaProxy.url() |> MediaProxy.url()
end end
def last_activity(activities) do def last_activity(activities), do: List.last(activities)
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 end
def activity_object(activity) do def activity_content(%{data: %{"content" => content}}) 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"]
content content
|> String.replace(~r/[\n\r]/, "") |> String.replace(~r/[\n\r]/, "")
|> escape() |> escape()
end end
def activity_context(activity) do def activity_context(activity), do: activity.data["context"]
activity.data["context"]
end
def attachment_href(attachment) do def attachment_href(attachment) do
attachment["url"] attachment["url"]

View file

@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@relations [:follow, :unfollow] @relations [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @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, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
plug(RateLimiter, :relations_actions when action in @relations) plug(RateLimiter, [name: :relations_actions] when action in @relations)
plug(RateLimiter, :app_account_creation when action == :create) plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account) plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
@local_mastodon_name "Mastodon-Local" @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" @doc "GET /web/login"
def login(%{assigns: %{user: %User{}}} = conn, _params) do def login(%{assigns: %{user: %User{}}} = conn, _params) do

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) 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 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user)) accounts = User.search(query, search_options(params, user))

View file

@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug( plug(
RateLimiter, 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 when action in ~w(reblog unreblog)a
) )
plug( plug(
RateLimiter, 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 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) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

View file

@ -71,7 +71,6 @@ def public(%{assigns: %{user: user}} = conn, params) do
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn conn
|> add_link_headers(activities, %{"local" => local_only}) |> 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_all", tag_all)
|> Map.put("tag_reject", tag_reject) |> Map.put("tag_reject", tag_reject)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn conn
|> add_link_headers(activities, %{"local" => local_only}) |> add_link_headers(activities, %{"local" => local_only})

View file

@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
plug(RateLimiter, :authentication when action in [:user_exists, :check_password]) plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password) plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
def user_exists(conn, %{"user" => username}) do def user_exists(conn, %{"user" => username}) do
with %User{} <- Repo.get_by(User, nickname: username, local: true) do with %User{} <- Repo.get_by(User, nickname: username, local: true) do

View file

@ -46,6 +46,7 @@ def raw_nodeinfo do
data data
|> Map.merge(%{quarantined_instances: quarantined}) |> Map.merge(%{quarantined_instances: quarantined})
|> Map.put(:enabled, Config.get([:instance, :federating]))
else else
%{} %{}
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper alias Pleroma.Helpers.UriHelper
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration alias Pleroma.Registration
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_session) plug(:fetch_session)
plug(:fetch_flash) 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) 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)) authorize(conn, Map.merge(params, auth_attrs))
end 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 if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params) do_authorize(conn, params)
else else
@ -44,6 +45,22 @@ def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
end end
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) def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do defp do_authorize(%Plug.Conn{} = conn, params) do

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Fallback.RedirectController alias Fallback.RedirectController
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Router alias Pleroma.Web.Router
plug( plug(
Pleroma.Plugs.RateLimiter, RateLimiter,
{:ap_routes, params: ["uuid"]} when action in [:object, :activity] [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
) )
plug( plug(

View file

@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
when action != :confirmation_resend 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(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)

View file

@ -495,6 +495,7 @@ defmodule Pleroma.Web.Router do
pipeline :ostatus do pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"]) plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
plug(Pleroma.Plugs.StaticFEPlug)
end end
pipeline :oembed do pipeline :oembed do

View file

@ -0,0 +1,124 @@
# 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
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
_ ->
conn
|> put_status(404)
|> render("error.html", %{message: "Post not found.", meta: ""})
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
})
_ ->
conn
|> put_status(404)
|> render("error.html", %{message: "User not found.", meta: ""})
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(conn, _opts), do: conn
end

View 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

View file

@ -2,11 +2,13 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id> <id><%= @data["id"] %></id>
<title><%= "New note by #{@user.nickname}" %></title> <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@activity) %></content> <content type="html"><%= activity_content(@object) %></content>
<published><%= @data["published"] %></published> <published><%= @data["published"] %></published>
<updated><%= @data["published"] %></updated> <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"/> <link ref="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] do %> <%= if @data["summary"] do %>

View file

@ -19,6 +19,6 @@
<% end %> <% end %>
<%= for activity <- @activities do %> <%= 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 %> <% end %>
</feed> </feed>

View 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>

View file

@ -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 %>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,7 @@
<header>
<h1><%= gettext("Oops") %></h1>
</header>
<main>
<p><%= @message %></p>
</main>

View file

@ -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>

View file

@ -155,7 +155,6 @@ defp deps do
{:joken, "~> 2.0"}, {:joken, "~> 2.0"},
{:benchee, "~> 1.0"}, {:benchee, "~> 1.0"},
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
{:ex_rated, "~> 1.3"},
{:ex_const, "~> 0.2"}, {:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"}, {:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.11.1", only: :test}, {:excoveralls, "~> 0.11.1", only: :test},

View file

@ -33,7 +33,6 @@
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"}, "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_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_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"]}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
"excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"}, "fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"},

View file

@ -0,0 +1,176 @@
body {
background-color: #282c37;
font-family: sans-serif;
color: white;
}
main {
margin: 50px auto;
max-width: 960px;
padding: 40px;
background-color: #313543;
border-radius: 4px;
}
header {
margin: 50px auto;
max-width: 960px;
padding: 40px;
background-color: #313543;
border-radius: 4px;
}
.activity {
border-radius: 4px;
padding: 1em;
padding-bottom: 2em;
margin-bottom: 1em;
}
.avatar {
cursor: pointer;
}
.avatar img {
float: left;
border-radius: 4px;
margin-right: 4px;
}
.activity-content img, video, audio {
padding: 1em;
max-width: 800px;
max-height: 800px;
}
#selected {
background-color: #1b2735;
}
.counts dt, .counts dd {
float: left;
margin-left: 1em;
}
a {
color: white;
}
.h-card {
min-height: 48px;
margin-bottom: 8px;
}
header a, .h-card a {
text-decoration: none;
}
header a:hover, .h-card a:hover {
text-decoration: underline;
}
.display-name {
padding-top: 4px;
display: block;
text-overflow: ellipsis;
overflow: hidden;
color: white;
}
/* keep emoji from being hilariously huge */
.display-name img {
max-height: 1em;
}
.display-name .nickname {
padding-top: 4px;
display: block;
}
.nickname:hover {
text-decoration: none;
}
.pull-right {
float: right;
}
.collapse {
margin: 0;
width: auto;
}
h1 {
margin: 0;
}
h2 {
color: #9baec8;
font-weight: normal;
font-size: 20px;
margin-bottom: 40px;
}
form {
width: 100%;
}
input {
box-sizing: border-box;
width: 100%;
padding: 10px;
margin-top: 20px;
background-color: rgba(0,0,0,.1);
color: white;
border: 0;
border-bottom: 2px solid #9baec8;
font-size: 14px;
}
input:focus {
border-bottom: 2px solid #4b8ed8;
}
input[type="checkbox"] {
width: auto;
}
button {
box-sizing: border-box;
width: 100%;
color: white;
background-color: #419bdd;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 30px;
text-transform: uppercase;
font-weight: 500;
font-size: 16px;
}
.alert-danger {
box-sizing: border-box;
width: 100%;
color: #D8000C;
background-color: #FFD2D2;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}
.alert-info {
box-sizing: border-box;
width: 100%;
color: #00529B;
background-color: #BDE5F8;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 20px;
font-weight: 500;
font-size: 16px;
}

View file

@ -12,163 +12,196 @@ defmodule Pleroma.Plugs.RateLimiterTest do
# Note: each example must work with separate buckets in order to prevent concurrency issues # Note: each example must work with separate buckets in order to prevent concurrency issues
test "init/1" do describe "config" do
limiter_name = :test_init test "config is required for plug to work" do
Pleroma.Config.put([:rate_limit, limiter_name], {1, 1}) limiter_name = :test_init
Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name) assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
assert nil == RateLimiter.init(:foo) RateLimiter.init(name: limiter_name)
assert nil == RateLimiter.init(name: :foo)
end
test "it restricts based on config values" do
limiter_name = :test_opts
scale = 60
limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
opts = RateLimiter.init(name: limiter_name)
conn = conn(:get, "/")
for i <- 1..5 do
conn = RateLimiter.call(conn, opts)
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
Process.sleep(10)
end
conn = RateLimiter.call(conn, opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
Process.sleep(50)
conn = conn(:get, "/")
conn = RateLimiter.call(conn, opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
refute conn.resp_body
refute conn.halted
end
end end
test "ip/1" do describe "options" do
assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}}) test "`bucket_name` option overrides default bucket name" do
limiter_name = :test_bucket_name
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
base_bucket_name = "#{limiter_name}:group1"
opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
conn = conn(:get, "/")
RateLimiter.call(conn, opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
end
test "`params` option allows different queries to be tracked independently" do
limiter_name = :test_params
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
opts = RateLimiter.init(name: limiter_name, params: ["id"])
conn = conn(:get, "/?id=1")
conn = Plug.Conn.fetch_query_params(conn)
conn_2 = conn(:get, "/?id=2")
RateLimiter.call(conn, opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
end
test "it supports combination of options modifying bucket name" do
limiter_name = :test_options_combo
Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
base_bucket_name = "#{limiter_name}:group1"
opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
id = "100"
conn = conn(:get, "/?id=#{id}")
conn = Plug.Conn.fetch_query_params(conn)
conn_2 = conn(:get, "/?id=#{101}")
RateLimiter.call(conn, opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
end
end end
test "it restricts by opts" do describe "unauthenticated users" do
limiter_name = :test_opts test "are restricted based on remote IP" do
scale = 1000 limiter_name = :test_unauthenticated
limit = 5 Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) opts = RateLimiter.init(name: limiter_name)
opts = RateLimiter.init(limiter_name) conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
conn = conn(:get, "/") conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
conn = RateLimiter.call(conn, opts) for i <- 1..5 do
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) conn = RateLimiter.call(conn, opts)
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
refute conn.halted
end
conn = RateLimiter.call(conn, opts) conn = RateLimiter.call(conn, opts)
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
conn = RateLimiter.call(conn, opts) assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) assert conn.halted
conn = RateLimiter.call(conn, opts) conn_2 = RateLimiter.call(conn_2, opts)
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
conn = RateLimiter.call(conn, opts) refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) refute conn_2.resp_body
refute conn_2.halted
conn = RateLimiter.call(conn, opts) end
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
Process.sleep(to_reset)
conn = conn(:get, "/")
conn = RateLimiter.call(conn, opts)
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
refute conn.resp_body
refute conn.halted
end end
test "`bucket_name` option overrides default bucket name" do describe "authenticated users" do
limiter_name = :test_bucket_name setup do
scale = 1000 Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) :ok
base_bucket_name = "#{limiter_name}:group1" end
opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
conn = conn(:get, "/") test "can have limits seperate from unauthenticated connections" do
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}" limiter_name = :test_authenticated
customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
RateLimiter.call(conn, opts) scale = 1000
assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit) limit = 5
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit) Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
end
test "`params` option appends specified params' values to bucket name" do opts = RateLimiter.init(name: limiter_name)
limiter_name = :test_params
scale = 1000
limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) user = insert(:user)
opts = RateLimiter.init({limiter_name, params: ["id"]}) conn = conn(:get, "/") |> assign(:user, user)
id = "1"
conn = conn(:get, "/?id=#{id}") for i <- 1..5 do
conn = Plug.Conn.fetch_query_params(conn) conn = RateLimiter.call(conn, opts)
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
refute conn.halted
end
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}" conn = RateLimiter.call(conn, opts)
parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
RateLimiter.call(conn, opts) assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit) assert conn.halted
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
end
test "it supports combination of options modifying bucket name" do Process.sleep(1550)
limiter_name = :test_options_combo
scale = 1000
limit = 5
Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit}) conn = conn(:get, "/") |> assign(:user, user)
base_bucket_name = "#{limiter_name}:group1" conn = RateLimiter.call(conn, opts)
opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]}) assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
id = "100"
conn = conn(:get, "/?id=#{id}") refute conn.status == Plug.Conn.Status.code(:too_many_requests)
conn = Plug.Conn.fetch_query_params(conn) refute conn.resp_body
refute conn.halted
end
default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}" test "diffrerent users are counted independently" do
parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}" limiter_name = :test_authenticated
Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
RateLimiter.call(conn, opts) opts = RateLimiter.init(name: limiter_name)
assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
end
test "optional limits for authenticated users" do user = insert(:user)
limiter_name = :test_authenticated conn = conn(:get, "/") |> assign(:user, user)
Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
scale = 1000 user_2 = insert(:user)
limit = 5 conn_2 = conn(:get, "/") |> assign(:user, user_2)
Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
opts = RateLimiter.init(limiter_name) for i <- 1..5 do
conn = RateLimiter.call(conn, opts)
assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
end
user = insert(:user) conn = RateLimiter.call(conn, opts)
conn = conn(:get, "/") |> assign(:user, user) assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
bucket_name = "#{limiter_name}:#{user.id}" assert conn.halted
conn = RateLimiter.call(conn, opts) conn_2 = RateLimiter.call(conn_2, opts)
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
conn = RateLimiter.call(conn, opts) refute conn_2.resp_body
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit) refute conn_2.halted
end
conn = RateLimiter.call(conn, opts)
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
conn = RateLimiter.call(conn, opts)
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
conn = RateLimiter.call(conn, opts)
assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
conn = RateLimiter.call(conn, opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
assert conn.halted
Process.sleep(to_reset)
conn = conn(:get, "/") |> assign(:user, user)
conn = RateLimiter.call(conn, opts)
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
refute conn.status == Plug.Conn.Status.code(:too_many_requests)
refute conn.resp_body
refute conn.halted
end end
end end

View file

@ -734,56 +734,54 @@ test "retrieves public activities" do
end end
test "retrieves a maximum of 20 activities" do test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30) ActivityBuilder.insert_list(10)
last_expected = List.last(activities) expected_activities = ActivityBuilder.insert_list(20)
activities = ActivityPub.fetch_public_activities() activities = ActivityPub.fetch_public_activities()
last = List.last(activities)
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 20 assert length(activities) == 20
assert last == last_expected
end end
test "retrieves ids starting from a since_id" do test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30) activities = ActivityBuilder.insert_list(30)
later_activities = ActivityBuilder.insert_list(10) expected_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id since_id = List.last(activities).id
last_expected = List.last(later_activities)
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id}) activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
last = List.last(activities)
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 10 assert length(activities) == 10
assert last == last_expected
end end
test "retrieves ids up to max_id" do test "retrieves ids up to max_id" do
_first_activities = ActivityBuilder.insert_list(10) ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(20) expected_activities = ActivityBuilder.insert_list(20)
later_activities = ActivityBuilder.insert_list(10)
max_id = List.first(later_activities).id %{id: max_id} =
last_expected = List.last(activities) 10
|> ActivityBuilder.insert_list()
|> List.first()
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id}) activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
last = List.last(activities)
assert length(activities) == 20 assert length(activities) == 20
assert last == last_expected assert collect_ids(activities) == collect_ids(expected_activities)
end end
test "paginates via offset/limit" do test "paginates via offset/limit" do
_first_activities = ActivityBuilder.insert_list(10) _first_part_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(10) second_part_activities = ActivityBuilder.insert_list(10)
_later_activities = ActivityBuilder.insert_list(10)
first_expected = List.first(activities) later_activities = ActivityBuilder.insert_list(10)
activities = activities =
ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset) ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
first = List.first(activities)
assert length(activities) == 20 assert length(activities) == 20
assert first == first_expected
assert collect_ids(activities) ==
collect_ids(second_part_activities) ++ collect_ids(later_activities)
end end
test "doesn't return reblogs for users for whom reblogs have been muted" do test "doesn't return reblogs for users for whom reblogs have been muted" do

View file

@ -2269,6 +2269,10 @@ test "delete part of settings by atom subkeys", %{conn: conn} do
Pleroma.Config.put([:instance, :dynamic_configuration], true) Pleroma.Config.put([:instance, :dynamic_configuration], true)
end end
clear_config([:feed, :post_title]) do
Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: ""})
end
test "transfer settings to DB and to file", %{conn: conn, admin: admin} do test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")

View file

@ -6,16 +6,25 @@ defmodule Pleroma.Web.Feed.FeedControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
import Pleroma.Factory import Pleroma.Factory
import SweetXml
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
clear_config([:feed])
test "gets a feed", %{conn: conn} do test "gets a feed", %{conn: conn} do
Pleroma.Config.put(
[:feed, :post_title],
%{max_length: 10, omission: "..."}
)
activity = insert(:note_activity) activity = insert(:note_activity)
note = note =
insert(:note, insert(:note,
data: %{ data: %{
"content" => "This is :moominmamma: note ",
"attachment" => [ "attachment" => [
%{ %{
"url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"}] "url" => [%{"mediaType" => "image/png", "href" => "https://pleroma.gov/image.png"}]
@ -26,15 +35,30 @@ test "gets a feed", %{conn: conn} do
) )
note_activity = insert(:note_activity, note: note) note_activity = insert(:note_activity, note: note)
object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"]) user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn = note2 =
insert(:note,
user: user,
data: %{"content" => "42 This is :moominmamma: note ", "inReplyTo" => activity.data["id"]}
)
_note_activity2 = insert(:note_activity, note: note2)
object = Object.normalize(note_activity)
resp =
conn conn
|> put_req_header("content-type", "application/atom+xml") |> put_req_header("content-type", "application/atom+xml")
|> get("/users/#{user.nickname}/feed.atom") |> get("/users/#{user.nickname}/feed.atom")
|> response(200)
assert response(conn, 200) =~ object.data["content"] activity_titles =
resp
|> SweetXml.parse()
|> SweetXml.xpath(~x"//entry/title/text()"l)
assert activity_titles == ['42 This...', 'This is...']
assert resp =~ object.data["content"]
end end
test "returns 404 for a missing feed", %{conn: conn} do test "returns 404 for a missing feed", %{conn: conn} do

View file

@ -84,6 +84,30 @@ test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
Pleroma.Config.put([:instance, :safe_dm_mentions], option) Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end end
test "it shows if federation is enabled/disabled", %{conn: conn} do
original = Pleroma.Config.get([:instance, :federating])
Pleroma.Config.put([:instance, :federating], true)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert response["metadata"]["federation"]["enabled"] == true
Pleroma.Config.put([:instance, :federating], false)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert response["metadata"]["federation"]["enabled"] == false
Pleroma.Config.put([:instance, :federating], original)
end
test "it shows MRF transparency data if enabled", %{conn: conn} do test "it shows MRF transparency data if enabled", %{conn: conn} do
config = Pleroma.Config.get([:instance, :rewrite_policy]) config = Pleroma.Config.get([:instance, :rewrite_policy])
Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])

View file

@ -469,6 +469,29 @@ test "renders authentication page if user is already authenticated but `force_lo
assert html_response(conn, 200) =~ ~s(type="submit") assert html_response(conn, 200) =~ ~s(type="submit")
end end
test "renders authentication page if user is already authenticated but user request with another client",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app_id: app.id)
conn =
conn
|> put_session(:oauth_token, token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => "another_client_id",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params", test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{ %{
app: app, app: app,

View file

@ -0,0 +1,181 @@
defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
clear_config_all([:static_fe, :enabled]) do
Pleroma.Config.put([:static_fe, :enabled], true)
end
describe "user profile page" do
test "just the profile as HTML", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/users/#{user.nickname}")
assert html_response(conn, 200) =~ user.nickname
end
test "renders json unless there's an html accept header", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}")
assert json_response(conn, 200)
end
test "404 when user not found", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/users/limpopo")
assert html_response(conn, 404) =~ "not found"
end
test "profile does not include private messages", %{conn: conn} do
user = insert(:user)
CommonAPI.post(user, %{"status" => "public"})
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/users/#{user.nickname}")
html = html_response(conn, 200)
assert html =~ ">public<"
refute html =~ ">private<"
end
test "pagination", %{conn: conn} do
user = insert(:user)
Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/users/#{user.nickname}")
html = html_response(conn, 200)
assert html =~ ">test30<"
assert html =~ ">test11<"
refute html =~ ">test10<"
refute html =~ ">test1<"
end
test "pagination, page 2", %{conn: conn} do
user = insert(:user)
activities = Enum.map(1..30, fn i -> CommonAPI.post(user, %{"status" => "test#{i}"}) end)
{:ok, a11} = Enum.at(activities, 11)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/users/#{user.nickname}?max_id=#{a11.id}")
html = html_response(conn, 200)
assert html =~ ">test1<"
assert html =~ ">test10<"
refute html =~ ">test20<"
refute html =~ ">test29<"
end
end
describe "notice rendering" do
test "single notice page", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "testing a thing!"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ "<header>"
assert html =~ user.nickname
assert html =~ "testing a thing!"
end
test "shows the whole thread", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "space: the final frontier"})
CommonAPI.post(user, %{
"status" => "these are the voyages or something",
"in_reply_to_status_id" => activity.id
})
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
html = html_response(conn, 200)
assert html =~ "the final frontier"
assert html =~ "voyages"
end
test "404 when notice not found", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/88c9c317")
assert html_response(conn, 404) =~ "not found"
end
test "404 for private status", %{conn: conn} do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "don't show me!", "visibility" => "private"})
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
assert html_response(conn, 404) =~ "not found"
end
test "404 for remote cached status", %{conn: conn} do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => user.follower_address,
"cc" => "https://www.w3.org/ns/activitystreams#Public",
"type" => "Create",
"object" => %{
"content" => "blah blah blah",
"type" => "Note",
"attributedTo" => user.ap_id,
"inReplyTo" => nil
},
"actor" => user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{activity.id}")
assert html_response(conn, 404) =~ "not found"
end
end
end