Merge branch 'develop' into pleroma-database-config-whitelist

This commit is contained in:
nicole mikołajczyk 2026-03-01 22:44:08 +00:00
commit c3b779036d
57 changed files with 1378 additions and 611 deletions

View file

@ -0,0 +1,25 @@
name: 'Bug report'
about: 'Report a bug in Pleroma'
body:
- type: markdown
attributes:
value: |
### Precheck
* For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels).
* Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it.
* Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository.
- type: textarea
id: environment
attributes:
label: Environment
value: |
* Installation type (OTP or From Source):
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system:
* PostgreSQL version (`psql -V`):
- type: textarea
id: bug-description
attributes:
label: Bug description

View file

@ -0,0 +1,13 @@
### Checklist
- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `<code>.<type>`.
<!--
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the PR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
In the file, write the changelog entry. For example, if a PR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
If one changelog entry is not enough, you may add more. But that might mean you can split it into two PRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring).
-->

28
.woodpecker.yml Normal file
View file

@ -0,0 +1,28 @@
when:
- event:
- pull_request
steps:
test:
image: elixir:1.15-alpine
environment:
MIX_ENV: test
DB_HOST: postgres
DB_PORT: 5432
commands:
- apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
- adduser -D -h /home/testuser testuser
- mkdir -p /home/testuser/.mix /home/testuser/.hex
- chown -R testuser:testuser . /home/testuser
- su testuser -c "HOME=/home/testuser mix local.hex --force"
- su testuser -c "HOME=/home/testuser mix local.rebar --force"
- su testuser -c "HOME=/home/testuser mix deps.get"
- su testuser -c "HOME=/home/testuser mix test"
services:
postgres:
image: postgres:13-alpine
environment:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

View file

@ -1,4 +1,4 @@
<img src="https://git.pleroma.social/pleroma/pleroma/uploads/8cec84f5a084d887339f57deeb8a293e/pleroma-banner-vector-nopad-notext.svg" width="300px" />
<img src="https://git.pleroma.social/attachments/06a95f5a-7cac-42ad-8b1d-1483f1739f38" width="300px" />
## About
@ -19,8 +19,6 @@ If you are running Linux (glibc or musl) on x86/arm, the recommended way to inst
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.
- [Alpine Linux](https://docs-develop.pleroma.social/backend/installation/alpine_linux_en/)
- [Arch Linux](https://docs-develop.pleroma.social/backend/installation/arch_linux_en/)
- [CentOS 7](https://docs-develop.pleroma.social/backend/installation/centos7_en/)
- [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/)
- [FreeBSD](https://docs-develop.pleroma.social/backend/installation/freebsd_en/)

View file

@ -0,0 +1 @@
Allow assigning users to reports

View file

@ -0,0 +1 @@
Move avatar_description and header_description fields to the account object

View file

@ -0,0 +1 @@
Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney

View file

@ -0,0 +1 @@
Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation

View file

@ -0,0 +1 @@
Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS)

View file

@ -0,0 +1 @@
Add /api/v2/instance profile fields limits info used by Mastodon

View file

View file

View file

@ -0,0 +1 @@
DB prune: Check if user follows hashtag with no objects before deletion

View file

@ -0,0 +1 @@
Add mute/block expiry to the relationship object

View file

@ -665,6 +665,7 @@ Status: 404
- *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `assigned_account`: **string** assigned account ID
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
@ -749,6 +750,7 @@ Status: 404
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"assigned_account": null,
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
@ -868,6 +870,37 @@ Status: 404
]
```
- Response:
- On failure:
- 400 Bad Request, JSON:
```json
[
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/v1/pleroma/admin/reports/assign_account`
### Assign account to one or multiple reports
- Params:
```json
`reports`: [
{
`id`, // required, report id
`nickname` // account nickname, use null to unassign account
},
...
]
```
- Response:
- On failure:
- 400 Bad Request, JSON:

View file

@ -127,8 +127,6 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
- `avatar_description`: string, image description for user avatar, defaults to empty string
- `header_description`: string, image description for user banner, defaults to empty string
### Source

View file

@ -690,6 +690,7 @@ Audio scrobbling in Pleroma is **deprecated**.
* `album`: the album of the media playing [optional]
* `artist`: the artist of the media playing [optional]
* `length`: the length of the media playing [optional]
* `external_link`: a URL referencing the media playing [optional]
* Response: the newly created media metadata entity representing the Listen activity
# Emoji Reactions

View file

@ -1,226 +0,0 @@
# Installing on Arch Linux
{! backend/installation/otp_vs_from_source_source.include !}
## Installation
This guide will assume that you have administrative rights, either as root or a user with [sudo permissions](https://wiki.archlinux.org/index.php/Sudo). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su <username> -s $SHELL -c 'command'` instead.
### Required packages
* `postgresql`
* `elixir`
* `git`
* `base-devel`
* `cmake`
* `file`
#### Optional packages used in this guide
* `nginx` (preferred, example configs for other reverse proxies can be found in the repo)
* `certbot` (or any other ACME client for Lets Encrypt certificates)
* `ImageMagick`
* `ffmpeg`
* `exiftool`
### Prepare the system
* First update the system, if not already done:
```shell
sudo pacman -Syu
```
* Install some of the above mentioned programs:
```shell
sudo pacman -S git base-devel elixir cmake file
```
### Install PostgreSQL
[Arch Wiki article](https://wiki.archlinux.org/index.php/PostgreSQL)
* Install the `postgresql` package:
```shell
sudo pacman -S postgresql
```
* Initialize the database cluster:
```shell
sudo -iu postgres initdb -D /var/lib/postgres/data
```
* Start and enable the `postgresql.service`
```shell
sudo systemctl enable --now postgresql.service
```
### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md))
```shell
sudo pacman -S ffmpeg imagemagick perl-image-exiftool
```
### Install PleromaBE
* Add a new system user for the Pleroma service:
```shell
sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```
**Note**: To execute a single command as the Pleroma system user, use `sudo -Hu pleroma command`. You can also switch to a shell by using `sudo -Hu pleroma $SHELL`. If you dont have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l pleroma -s $SHELL -c 'command'` and `su -l pleroma -s $SHELL` for starting a shell.
* Git clone the PleromaBE repository and make the Pleroma user the owner of the directory:
```shell
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* Change to the new directory:
```shell
cd /opt/pleroma
```
* Install the dependencies for Pleroma and answer with `yes` if it asks you to install `Hex`:
```shell
sudo -Hu pleroma mix deps.get
```
* Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen`
* Answer with `yes` if it asks you to install `rebar3`.
* This may take some time, because parts of pleroma get compiled first.
* After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`.
* Check the configuration and if all looks right, rename it, so Pleroma will load it (`prod.secret.exs` for productive instance, `dev.secret.exs` for development instances):
```shell
sudo -Hu pleroma mv config/{generated_config.exs,prod.secret.exs}
```
* The previous command creates also the file `config/setup_db.psql`, with which you can create the database:
```shell
sudo -Hu postgres psql -f config/setup_db.psql
```
* Now run the database migration:
```shell
sudo -Hu pleroma MIX_ENV=prod mix ecto.migrate
```
* Now you can start Pleroma already
```shell
sudo -Hu pleroma MIX_ENV=prod mix phx.server
```
### Finalize installation
If you want to open your newly installed instance to the world, you should run nginx or some other webserver/proxy in front of Pleroma and you should consider to create a systemd service file for Pleroma.
#### Nginx
* Install nginx, if not already done:
```shell
sudo pacman -S nginx
```
* Create directories for available and enabled sites:
```shell
sudo mkdir -p /etc/nginx/sites-{available,enabled}
```
* Append the following line at the end of the `http` block in `/etc/nginx/nginx.conf`:
```Nginx
include sites-enabled/*;
```
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
```shell
sudo pacman -S certbot certbot-nginx
```
and then set it up:
```shell
sudo mkdir -p /var/lib/letsencrypt/
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
```
If that doesnt work, make sure, that nginx is not already running. If it still doesnt work, try setting up nginx first (change ssl “on” to “off” and try again).
---
* Copy the example nginx configuration and activate it:
```shell
sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx
sudo ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
```
* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
* (Strongly recommended) serve media on another domain
Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors.
* Enable and start nginx:
```shell
sudo systemctl enable --now nginx.service
```
If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run:
```shell
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
```
#### Other webserver/proxies
You can find example configurations for them in `/opt/pleroma/installation/`.
#### Systemd service
* Copy example service file
```shell
sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
```
* Edit the service file and make sure that all paths fit your installation
* Enable and start `pleroma.service`:
```shell
sudo systemctl enable --now pleroma.service
```
#### Create your first user
If your instance is up and running, you can create your first user with administrative rights with the following task:
```shell
sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
```
#### Further reading
{! backend/installation/further_reading.include !}
## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -1,294 +0,0 @@
# Installing on NetBSD
{! backend/installation/generic_dependencies.include !}
# Installation options
Currently there are two options available for NetBSD: manual installation (from source) or using experimental package from [pkgsrc-wip](https://github.com/NetBSD/pkgsrc-wip/tree/master/pleroma).
WIP package can be installed via pkgsrc and can be crosscompiled for easier binary distribution. Source installation most probably will be restricted to a single machine.
## pkgsrc installation
WIP package creates Mix.Release (similar to how Docker images are built) but doesn't bundle Erlang runtime, listing it as a dependency instead. This allows for easier and more modular installations, especially on weaker machines. Currently this method also does not support all features of `pleroma_ctl` command (like changing installation type or managing frontends) as NetBSD is not yet a supported binary flavour of Pleroma's CI.
In any case, you can install it the same way as any other `pkgsrc-wip` package:
```
cd /usr/pkgsrc
git clone --depth 1 git://wip.pkgsrc.org/pkgsrc-wip.git wip
cp -rf wip/pleroma www
cp -rf wip/libvips graphics
cd /usr/pkgsrc/www/pleroma
bmake && bmake install
```
Use `bmake package` to create a binary package. This can come especially handy if you're targeting embedded or low-power systems and are crosscompiling on a more powerful machine.
> Note: Elixir has [endianness bug](https://github.com/elixir-lang/elixir/issues/2785) which requires it to be compiled on a machine with the same endianness. In other words, package crosscompiled on amd64 (little endian) won't work on powerpc or sparc machines (big endian). While _in theory™_ nothing catastrophic should happen, one can see that for example regexes won't work properly. Some distributions just strip this warning away, so it doesn't bother the users... anyway, you've been warned.
## Source installation
pkgin should have been installed by the NetBSD installer if you selected
the right options. If it isn't installed, install it using `pkg_add`.
Note that `postgresql11-contrib` is needed for the Postgres extensions
Pleroma uses.
> Note: you can use modern versions of PostgreSQL. In this case, just use `postgresql16-contrib` and so on.
The `mksh` shell is needed to run the Elixir `mix` script.
`# pkgin install acmesh elixir git-base git-docs mksh nginx postgresql11-server postgresql11-client postgresql11-contrib sudo ffmpeg4 ImageMagick`
You can also build these packages using pkgsrc:
```
databases/postgresql11-contrib
databases/postgresql11-client
databases/postgresql11-server
devel/git-base
devel/git-docs
devel/cmake
lang/elixir
security/acmesh
security/sudo
shells/mksh
www/nginx
```
Create a user for Pleroma:
```
# groupadd pleroma
# useradd -d /home/pleroma -m -g pleroma -s /usr/pkg/bin/mksh pleroma
# echo 'export LC_ALL="en_GB.UTF-8"' >> /home/pleroma/.profile
# su -l pleroma -c $SHELL
```
Clone the repository:
```
$ cd /home/pleroma
$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git
```
Get deps and compile:
```
$ cd /home/pleroma/pleroma
$ export MIX_ENV=prod
$ mix deps.get
$ mix compile
```
## Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md))
`# pkgin install ImageMagick ffmpeg4 p5-Image-ExifTool`
or via pkgsrc:
```
graphics/p5-Image-ExifTool
graphics/ImageMagick
multimedia/ffmpeg4
```
# Configuration
## Understanding $PREFIX
From now on, you may encounter `$PREFIX` variable in the paths. This variable indicates your current local pkgsrc prefix. Usually it's `/usr/pkg` unless you configured it otherwise. Translating to pkgsrc's lingo, it's called `LOCALBASE`, which essentially means the same this. You may want to set it up for your local shell session (this uses `mksh` which should already be installed as one of the required dependencies):
```
$ export PREFIX=$(pkg_info -Q LOCALBASE mksh)
$ echo $PREFIX
/usr/pkg
```
## Setting up your instance
Now, you need to configure your instance. During this initial configuration, you will be asked some questions about your server. You will need a domain name at this point; it doesn't have to be deployed, but changing it later will be very cumbersome.
If you've installed via pkgsrc, `pleroma_ctl` should already be in your `PATH`; if you've installed from source, it's located at `/home/pleroma/pleroma/release/bin/pleroma_ctl`.
```
$ su -l pleroma
$ pleroma_ctl instance gen --output $PREFIX/etc/pleroma/config.exs --output-psql /tmp/setup_db.psql
```
During installation, you will be asked about static and upload directories. Don't forget to create them and update permissions:
```
mkdir -p /var/lib/pleroma/uploads
chown -R pleroma:pleroma /var/lib/pleroma
```
## Setting up the database
First, run `# /etc/rc.d/pgsql start`. Then, `$ sudo -Hu pgsql -g pgsql createdb`.
We can now initialize the database. You'll need to edit generated SQL file from the previous step. It's located at `/tmp/setup_db.psql`.
Edit this file, and *change the password* to a password of your choice. Make sure it is secure, since
it'll be protecting your database. Now initialize the database:
```
$ sudo -Hu pgsql -g pgsql psql -f /tmp/setup_db.psql
```
Postgres allows connections from all users without a password by default. To
fix this, edit `$PREFIX/pgsql/data/pg_hba.conf`. Change every `trust` to
`password`.
Once this is done, restart Postgres with `# /etc/rc.d/pgsql restart`.
Run the database migrations.
### pkgsrc installation
```
pleroma_ctl migrate
```
### Source installation
You will need to do this whenever you update with `git pull`:
```
$ cd /home/pleroma/pleroma
$ MIX_ENV=prod mix ecto.migrate
```
## Configuring nginx
Install the example configuration file
(`$PREFIX/share/examples/pleroma/pleroma.nginx` or `/home/pleroma/pleroma/installation/pleroma.nginx`) to
`$PREFIX/etc/nginx.conf`.
Note that it will need to be wrapped in a `http {}` block. You should add
settings for the nginx daemon outside of the http block, for example:
```
user nginx nginx;
error_log /var/log/nginx/error.log;
worker_processes 4;
events {
}
```
Edit the defaults:
* Change `ssl_certificate` and `ssl_trusted_certificate` to
`/etc/nginx/tls/fullchain`.
* Change `ssl_certificate_key` to `/etc/nginx/tls/key`.
* Change `example.tld` to your instance's domain name.
### (Strongly recommended) serve media on another domain
Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors.
## Configuring acme.sh
We'll be using acme.sh in Stateless Mode for TLS certificate renewal.
First, get your account fingerprint:
```
$ sudo -Hu nginx -g nginx acme.sh --register-account
```
You need to add the following to your nginx configuration for the server
running on port 80:
```
location ~ ^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)$ {
default_type text/plain;
return 200 "$1.6fXAG9VyG0IahirPEU2ZerUtItW2DHzDzD9wZaEKpqd";
}
```
Replace the string after after `$1.` with your fingerprint.
Start nginx:
```
# /etc/rc.d/nginx start
```
It should now be possible to issue a cert (replace `example.com`
with your domain name):
```
$ sudo -Hu nginx -g nginx acme.sh --issue -d example.com --stateless
```
Let's add auto-renewal to `/etc/daily.local`
(replace `example.com` with your domain):
```
/usr/pkg/bin/sudo -Hu nginx -g nginx \
/usr/pkg/sbin/acme.sh -r \
-d example.com \
--cert-file /etc/nginx/tls/cert \
--key-file /etc/nginx/tls/key \
--ca-file /etc/nginx/tls/ca \
--fullchain-file /etc/nginx/tls/fullchain \
--stateless
```
## Autostart
For properly functioning instance, you will need pleroma (backend service), nginx (reverse proxy) and postgresql (database) services running. There's no requirement for them to reside on the same machine, but you have to provide autostart for each of them.
### nginx
```
# cp $PREFIX/share/examples/rc.d/nginx /etc/rc.d
# echo "nginx=YES" >> /etc/rc.conf
```
### postgresql
```
# cp $PREFIX/share/examples/rc.d/pgsql /etc/rc.d
# echo "pgsql=YES" >> /etc/rc.conf
```
### pleroma
First, copy the script (pkgsrc variant)
```
# cp $PREFIX/share/examples/pleroma/pleroma.rc /etc/rc.d/pleroma
```
or source variant
```
# cp /home/pleroma/pleroma/installation/netbsd/rc.d/pleroma /etc/rc.d/pleroma
# chmod +x /etc/rc.d/pleroma
```
Then, add the following to `/etc/rc.conf`:
```
pleroma=YES
```
## Conclusion
Run `# /etc/rc.d/pleroma start` to start Pleroma.
Restart nginx with `# /etc/rc.d/nginx restart` and you should be up and running.
Make sure your time is in sync, or other instances will receive your posts with
incorrect timestamps. You should have ntpd running.
## Instances running NetBSD
* <https://catgirl.science>
#### Further reading
{! backend/installation/further_reading.include !}
## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -1,15 +0,0 @@
# Installing on NixOS
NixOS contains a source build package of pleroma and a NixOS module to install it.
For installation add this to your configuration.nix and add a config.exs next to it:
```nix
services.pleroma = {
enable = true;
configs = [ (lib.fileContents ./config.exs) ];
secretConfigFile = "/var/lib/pleroma/secret.exs";
};
```
## Questions
The nix community uses matrix for communication: [#nix:nixos.org](https://matrix.to/#/#nix:nixos.org)

View file

@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do
DELETE FROM hashtags AS ht
WHERE NOT EXISTS (
SELECT 1 FROM hashtags_objects hto
WHERE ht.id = hto.hashtag_id)
WHERE ht.id = hto.hashtag_id
)
AND NOT EXISTS (
SELECT 1 FROM user_follows_hashtag ufh
WHERE ht.id = ufh.hashtag_id
)
"""
|> Repo.query()

View file

@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do
else
{_, errors} ->
IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"]))
Enum.map(errors, &IO.puts/1)
Enum.each(errors, &IO.puts/1)
raise "Spec check failed"
end

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do
def invalidate_cache_for(activity_id) do
keys = get_cache_keys_for(activity_id)
Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
Enum.each(keys, &@cachex.del(:scrubber_cache, &1))
@cachex.del(:scrubber_management_cache, activity_id)
end

View file

@ -22,7 +22,8 @@ defmodule Pleroma.Constants do
"generator",
"rules",
"language",
"voters"
"voters",
"assigned_account"
]
)

View file

@ -6,8 +6,9 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
@behaviour Pleroma.HTTP.AdapterHelper
@defaults [
follow_redirect: true,
force_redirect: true
follow_redirect: false,
force_redirect: false,
with_body: true
]
@spec options(keyword(), URI.t()) :: keyword()

View file

@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do
end
def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
when action in ["report_note_delete", "report_update", "report_note"] do
when action in [
"report_note_delete",
"report_update",
"report_note",
"report_unassigned",
"report_assigned"
] do
data =
attrs
|> prepare_log_data
|> Pleroma.Maps.put_if_present("text", attrs[:text])
|> Pleroma.Maps.put_if_present("assigned_account", attrs[:assigned_account])
|> Map.merge(%{"subject" => report_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data})
@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do
" with '#{state}' state"
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_assigned",
"subject" => %{"id" => subject_id, "type" => "report"},
"assigned_account" => assigned_account
}
} = log
) do
"@#{actor_nickname} assigned report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" to user #{assigned_account}"
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_unassigned",
"subject" => %{"id" => subject_id, "type" => "report"}
}
} = log
) do
"@#{actor_nickname} unassigned report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" from a user"
end
def get_log_entry_message(
%ModerationLog{
data: %{

View file

@ -5,6 +5,23 @@
defmodule Pleroma.ReverseProxy.Client.Hackney do
@behaviour Pleroma.ReverseProxy.Client
# In-app redirect handler to avoid Hackney redirect bugs:
# - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney)
# - https://github.com/benoitc/hackney/issues/273 (redirects not followed when using HTTP proxy)
#
# Based on a redirect handler from Pleb, slightly modified to work with Hackney:
# https://declin.eu/objects/d4f38e62-5429-4614-86d1-e8fc16e6bf33
@redirect_statuses [301, 302, 303, 307, 308]
defp absolute_redirect_url(original_url, resp_headers) do
location =
Enum.find(resp_headers, fn {header, _location} ->
String.downcase(header) == "location"
end)
URI.merge(original_url, elem(location, 1))
|> URI.to_string()
end
@impl true
def request(method, url, headers, body, opts \\ []) do
opts =
@ -12,7 +29,35 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
path
end)
:hackney.request(method, url, headers, body, opts)
if opts[:follow_redirect] != false do
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
res = :hackney.request(method, url, headers, body, req_opts)
case res do
{:ok, code, resp_headers, _client} when code in @redirect_statuses ->
:hackney.request(
method,
absolute_redirect_url(url, resp_headers),
headers,
body,
req_opts
)
{:ok, code, resp_headers} when code in @redirect_statuses ->
:hackney.request(
method,
absolute_redirect_url(url, resp_headers),
headers,
body,
req_opts
)
_ ->
res
end
else
:hackney.request(method, url, headers, body, opts)
end
end
@impl true

View file

@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do
do: exists?(unquote(relationship_type), source, target)
# `def get_block_expire_date/2`, `def get_mute_expire_date/2`,
# `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`,
# `def get_reblog_mute_expire_date/2`, `def get_notification_mute_expire_date/2`,
# `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2`
def unquote(:"get_#{relationship_type}_expire_date")(source, target),
do: get_expire_date(unquote(relationship_type), source, target)

View file

@ -1003,6 +1003,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_state(query, _), do: query
defp restrict_assigned_account(query, %{assigned_account: assigned_account}) do
from(activity in query,
where: fragment("?->>'assigned_account' = ?", activity.data, ^assigned_account)
)
end
defp restrict_assigned_account(query, _), do: query
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
from(
[_activity, object] in query,
@ -1471,6 +1479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_assigned_account(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_blockers_visibility(opts)

View file

@ -27,7 +27,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
end
defp fetch(url) do
http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
# This module uses Tesla (Pleroma.HTTP) to fetch the MediaProxy URL.
# Redirect following is handled by Tesla middleware, so we must not enable
# adapter-level redirect logic (Hackney can crash on relative redirects when proxied).
http_client_opts =
[:media_proxy, :proxy_opts, :http]
|> Pleroma.Config.get(pool: :media)
|> Keyword.drop([:follow_redirect, :force_redirect])
HTTP.get(url, [], http_client_opts)
end

View file

@ -863,6 +863,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def update_report_state(_, _), do: {:error, "Unsupported state"}
def assign_report_to_account(%Activity{} = activity, nil = _account) do
new_data = Map.delete(activity.data, "assigned_account")
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(%Activity{} = activity, account) do
new_data = Map.put(activity.data, "assigned_account", account)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(activity_ids, account) do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def strip_report_status_data(%Activity{} = activity) do
with {:ok, new_data} <- strip_report_status_data(activity.data) do
{:ok, %{activity | data: new_data}}

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.ReportNote
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
plug(
OAuthScopesPlug,
%{scopes: ["admin:write:reports"]}
when action in [:update, :notes_create, :notes_delete]
when action in [:update, :assign_account, :notes_create, :notes_delete]
)
action_fallback(AdminAPI.FallbackController)
@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
def assign_account(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{reports: reports}}}
} = conn,
_
) do
result = Enum.map(reports, &do_assign_account(&1, admin))
if Enum.any?(result, &Map.has_key?(&1, :error)) do
json_response(conn, :bad_request, result)
else
json_response(conn, :no_content, "")
end
end
def notes_create(
%{
assigns: %{user: user},
@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
_ -> json_response(conn, :bad_request, "")
end
end
defp do_assign_account(%{assigned_account: nil, id: id}, admin) do
with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil),
report <- Activity.get_by_id_with_user_actor(activity.id) do
ModerationLog.insert_log(%{
action: "report_unassigned",
actor: admin,
subject: activity,
subject_actor: report.user_actor
})
activity
else
{:error, message} ->
%{id: id, error: message}
end
end
defp do_assign_account(%{assigned_account: assigned_account, id: id}, admin) do
with %User{id: account} = user <- User.get_cached_by_nickname(assigned_account),
{:ok, activity} <- CommonAPI.assign_report_to_account(id, account),
report <- Activity.get_by_id_with_user_actor(activity.id) do
ModerationLog.insert_log(%{
action: "report_assigned",
actor: admin,
subject: activity,
subject_actor: report.user_actor,
assigned_account: user.nickname
})
activity
else
{:error, message} ->
%{id: id, error: message}
end
end
end

View file

@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do
user = User.get_cached_by_ap_id(actor)
account = User.get_cached_by_ap_id(account_ap_id)
assigned_account =
if Map.has_key?(report.data, "assigned_account") do
User.get_cached_by_id(report.data["assigned_account"])
end
statuses =
status_ap_ids
|> Enum.reject(&is_nil(&1))
@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do
Activity.get_by_ap_id_with_object(act)
end)
%{report: report, user: user, account: account, statuses: statuses}
%{
report: report,
user: user,
account: account,
statuses: statuses,
assigned_account: assigned_account
}
end
defp make_fake_activity(act, user) do

View file

@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}
end
def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do
def render("show.json", %{
report: report,
user: user,
account: account,
statuses: statuses,
assigned_account: assigned_account
}) do
created_at = Utils.to_masto_date(report.data["published"])
content =
@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
nil
end
assigned_account =
if assigned_account do
merge_account_views(assigned_account)
end
%{
id: report.id,
account: merge_account_views(account),
@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}),
state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
rules: rules(Map.get(report.data, "rules", nil))
rules: rules(Map.get(report.data, "rules", nil)),
assigned_account: assigned_account
}
end

View file

@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
:query,
%Schema{type: :integer, default: 50},
"Number number of log entries per page"
),
Operation.parameter(
:assigned_account,
:query,
%Schema{type: :string},
"Filter by assigned account ID"
)
| admin_api_params()
],
@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
}
end
def assign_account_operation do
%Operation{
tags: ["Report management"],
summary: "Assign account to specified reports",
operationId: "AdminAPI.ReportController.assign_account",
security: [%{"oAuth" => ["admin:write:reports"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", assign_account_request(), required: true),
responses: %{
204 => no_content_response(),
400 => Operation.response("Bad Request", "application/json", update_400_response()),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def notes_create_operation do
%Operation{
tags: ["Report management"],
@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
hint: %Schema{type: :string, nullable: true}
}
}
}
},
assigned_account:
account_admin()
|> Map.put(:nullable, true)
}
}
end
@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
}
end
defp assign_account_request do
%Schema{
type: :object,
required: [:reports],
properties: %{
reports: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{allOf: [FlakeID], description: "Required, report ID"},
assigned_account: %Schema{
type: :string,
description: "User nickname",
nullable: true
}
}
},
example: %{
"reports" => [
%{"id" => "123", "assigned_account" => "pleroma"}
]
}
}
}
}
end
defp update_400_response do
%Schema{
type: :array,

View file

@ -342,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
max_pinned_statuses: %Schema{
type: :integer,
description: "The maximum number of pinned statuses for each account."
},
max_profile_fields: %Schema{
type: :integer,
description: "The maximum number of custom profile fields allowed to be set."
},
profile_field_name_limit: %Schema{
type: :integer,
description: "The maximum size of a profile field name, in characters."
},
profile_field_value_limit: %Schema{
type: :integer,
description: "The maximum size of a profile field value, in characters."
}
}
},

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
acct: %Schema{type: :string},
avatar_static: %Schema{type: :string, format: :uri},
avatar: %Schema{type: :string, format: :uri},
avatar_description: %Schema{type: :string},
bot: %Schema{type: :boolean},
created_at: %Schema{type: :string, format: "date-time"},
display_name: %Schema{type: :string},
@ -31,6 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
following_count: %Schema{type: :integer},
header_static: %Schema{type: :string, format: :uri},
header: %Schema{type: :string, format: :uri},
header_description: %Schema{type: :string},
id: FlakeID,
locked: %Schema{type: :boolean},
note: %Schema{type: :string, format: :html},
@ -111,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
nullable: true,
description: "Favicon image of the user's instance"
},
avatar_description: %Schema{type: :string},
header_description: %Schema{type: :string}
avatar_description: %Schema{type: :string, deprecated: true},
header_description: %Schema{type: :string, deprecated: true}
}
},
source: %Schema{

View file

@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean},
subscribing: %Schema{type: :boolean},
notifying: %Schema{type: :boolean}
notifying: %Schema{type: :boolean},
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
},
example: %{
"blocked_by" => false,

View file

@ -709,6 +709,22 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def assign_report_to_account(activity_ids, user) when is_list(activity_ids) do
case Utils.assign_report_to_account(activity_ids, user) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not assign account")}
end
end
def assign_report_to_account(activity_id, user) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.assign_report_to_account(activity, user)
else
nil -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not assign account")}
end
end
@spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),

View file

@ -96,6 +96,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
followed_by = FollowingRelationship.following?(target, reading_user)
following = FollowingRelationship.following?(reading_user, target)
blocking =
UserRelationship.exists?(
user_relationships,
:block,
reading_user,
target,
&User.blocks_user?(&1, &2)
)
muting =
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
)
requested =
cond do
following -> false
@ -116,14 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(target.id),
following: following,
followed_by: followed_by,
blocking:
UserRelationship.exists?(
user_relationships,
:block,
reading_user,
target,
&User.blocks_user?(&1, &2)
),
blocking: blocking,
blocked_by:
UserRelationship.exists?(
user_relationships,
@ -132,14 +143,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
reading_user,
&User.blocks_user?(&1, &2)
),
muting:
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
),
block_expires_at: nil,
muting: muting,
muting_notifications:
UserRelationship.exists?(
user_relationships,
@ -148,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
target,
&User.muted_notifications?(&1, &2)
),
mute_expires_at: nil,
subscribing: subscribing,
notifying: subscribing,
requested: requested,
@ -174,6 +180,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
&User.endorses?(&1, &2)
)
}
|> maybe_put_mute_expires_at(target, reading_user, %{mutes: muting})
|> maybe_put_block_expires_at(target, reading_user, %{blocks: blocking})
end
def render("relationships.json", %{user: user, targets: targets} = opts) do
@ -292,8 +300,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
note: user.bio,
url: user.uri || user.ap_id,
avatar: avatar,
avatar_description: avatar_description,
avatar_static: avatar_static,
header: header,
header_description: header_description,
header_static: header_static,
emojis: emojis,
fields: user.fields,
@ -343,8 +353,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
|> maybe_put_mute_expires_at(user, opts[:for], opts)
|> maybe_put_block_expires_at(user, opts[:for], opts)
|> maybe_put_mute_expires_at(user, opts[:for], opts, relationship)
|> maybe_put_block_expires_at(user, opts[:for], opts, relationship)
|> maybe_show_birthday(user, opts[:for])
end
@ -472,25 +482,47 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_email_address(data, _, _), do: data
defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do
defp maybe_put_mute_expires_at(data, target, user, opts, relationship \\ nil)
defp maybe_put_mute_expires_at(data, _target, _user, %{mutes: true}, %{
mute_expires_at: mute_expires_at
}) do
Map.put(data, :mute_expires_at, mute_expires_at)
end
defp maybe_put_mute_expires_at(data, %User{} = target, user, %{mutes: true}, _relationship) do
Map.put(
data,
:mute_expires_at,
UserRelationship.get_mute_expire_date(target, user)
UserRelationship.get_mute_expire_date(user, target)
)
end
defp maybe_put_mute_expires_at(data, _, _, _), do: data
defp maybe_put_mute_expires_at(data, _, _, _, _), do: data
defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do
defp maybe_put_block_expires_at(data, target, user, opts, relationship \\ nil)
defp maybe_put_block_expires_at(data, _target, _user, %{blocks: true}, %{
block_expires_at: block_expires_at
}) do
Map.put(data, :block_expires_at, block_expires_at)
end
defp maybe_put_block_expires_at(
data,
%User{} = target,
%User{} = user,
%{blocks: true},
_relationship
) do
Map.put(
data,
:block_expires_at,
UserRelationship.get_block_expire_date(target, user)
UserRelationship.get_block_expire_date(user, target)
)
end
defp maybe_put_block_expires_at(data, _, _, _), do: data
defp maybe_put_block_expires_at(data, _, _, _, _), do: data
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data

View file

@ -303,6 +303,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
defp configuration2 do
configuration()
|> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
|> put_in([:accounts, :max_profile_fields], Config.get([:instance, :max_account_fields]))
|> put_in(
[:accounts, :profile_field_name_limit],
Config.get([:instance, :account_field_name_length])
)
|> put_in(
[:accounts, :profile_field_value_limit],
Config.get([:instance, :account_field_value_length])
)
|> put_in([:statuses, :characters_reserved_per_url], 0)
|> Map.merge(%{
urls: %{

View file

@ -395,6 +395,7 @@ defmodule Pleroma.Web.Router do
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/assign_account", ReportController, :assign_account)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
end

View file

@ -154,7 +154,7 @@ defmodule Pleroma.Mixfile do
{:gun, "~> 2.2"},
{:finch, "~> 0.15"},
{:jason, "~> 1.2"},
{:mogrify, "~> 0.9.0", override: "true"},
{:mogrify, "~> 0.9.0", override: true},
{:ex_aws, "~> 2.1.6"},
{:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.7.2"},
@ -211,7 +211,7 @@ defmodule Pleroma.Mixfile do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.5", only: :test},
{:covertool, "~> 2.0", only: :test},
{:hackney, "~> 1.18.0", override: true},
{:hackney, "~> 1.25.0", override: true},
{:mox, "~> 1.0", only: :test},
{:websockex, "~> 0.4.3", only: :test},
{:benchee, "~> 1.0", only: :benchmark},

View file

@ -13,7 +13,7 @@
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]},
"castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
@ -59,7 +59,7 @@
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
"hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddActivityAssignedAccountIndex do
use Ecto.Migration
def change do
create_if_not_exists(
index(:activities, ["(data->>'assigned_account')"],
name: :activities_assigned_account_index
)
)
end
end

18
test/fixtures/server.pem vendored Normal file
View file

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQC0vCQAnSoGdzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjYwMTE2MTY1ODE5WhcNMzYwMTE0MTY1ODE5WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq
dZ4O2upZqwIo1eK5KrW1IIsjkfsFK8hE7Llh+4axcesiUKot0ib1CUhRSYiL1DLO
CIYQOw8IKQDVSC4JWAX9SsnX4W8dwexMQuSQG7/IKX2auC1bNNySFvoqM6Gq3GL9
MqBFonZGXDPZu8fmxsI/2p9+2GK13F+HXgoLlXSCoO3XELJaBmjv29tgxxWRxCiH
m4u0briSxgUEx+CctpKPvGDmLaoIOIhjtuoG6OjkeWUOp6jDcteazO23VxPyF5cS
NbRJgm8AckrTQ6wbWSnhyqF8rPEsIc0ZAlUdDEs5fL3sjugc566FvE+GOkZIEyDD
tgWbc4Ne+Kp/nnt6oVxpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADv+J1DTok8V
MKVKo0hsRnHTeJQ2+EIgOspuYlEzez3PysOZH6diAQxO2lzuo9LKxP3hnmw17XO/
P2oCzYyb9/P58VY/gr4UDIfuhgcE0cVfdsRhVId/I2FW6VP2f5q1TGbDUxSsVIlG
6hufn1aLBu90LtEbDkHqbnD05yYPwdqzWg4TrOXbX+jBhQrXJJdB3W7KTgozjRQw
F7+/2IyXoxXuxcwQBQlYhUbvGlsFqFpP/6cz2al5i5pNUkiNaSYwlRmuwa7zoTft
tHf57dhfXIpXET2BaJM6DSjDOOG/QleRXkvkTI5J21q+Bo+XnOzo19p4cZKJpTFC
SNgrftyNh3k=
-----END CERTIFICATE-----

View file

@ -8,6 +8,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Hashtag
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@ -550,6 +551,39 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
assert length(activities) == 3
end
test "it prunes hashtags with no objects associated", %{old_insert_date: old_insert_date} do
user = insert(:user)
{:ok, hashtag_post_activity} =
CommonAPI.post(user, %{status: "morning #cofe", local: true})
hashtag_post_object = Object.normalize(hashtag_post_activity)
{:ok, hashtag_post2_activity} =
CommonAPI.post(user, %{status: "morning #cawfee", local: true})
hashtag_post2_object = Object.normalize(hashtag_post2_activity)
hashtag_post_object
|> Ecto.Changeset.change(%{updated_at: old_insert_date})
|> Repo.update!()
hashtag_post2_object
|> Ecto.Changeset.change(%{updated_at: old_insert_date})
|> Repo.update!()
# Test whether hashtags with follow relationships are kept
User.follow_hashtag(user, Hashtag.get_by_name("cofe"))
assert length(Repo.all(Hashtag)) == 2
assert length(Repo.all(Object)) == 2
Mix.Tasks.Pleroma.Database.run(["prune_objects"])
assert length(Repo.all(Hashtag)) == 1
assert length(Repo.all(Object)) == 0
assert Repo.one(Hashtag) |> Map.fetch!(:name) == "cofe"
end
end
describe "running update_users_following_followers_counts" do

View file

@ -16,6 +16,14 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
describe "options/2" do
setup do: clear_config([:http, :adapter], a: 1, b: 2)
test "uses redirect-safe defaults", %{uri: uri} do
opts = Hackney.options([], uri)
assert opts[:follow_redirect] == false
assert opts[:force_redirect] == false
assert opts[:with_body] == true
end
test "add proxy and opts from config", %{uri: uri} do
opts = Hackney.options([proxy: "localhost:8123"], uri)

View file

@ -0,0 +1,355 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
use ExUnit.Case, async: false
setup do
{:ok, _} = Application.ensure_all_started(:hackney)
{:ok, tls_server} = start_tls_redirect_server()
{:ok, proxy} = start_connect_proxy()
on_exit(fn ->
stop_connect_proxy(proxy)
stop_tls_redirect_server(tls_server)
end)
{:ok, tls_server: tls_server, proxy: proxy}
end
test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true,
force_redirect: true
]
{pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end)
assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
match?({:function_clause, _}, reason)
end
test "redirects work via proxy when hackney follow_redirect is disabled", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
adapter_opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: false,
force_redirect: false,
with_body: true
]
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
end
test "reverse proxy hackney client follows redirects via proxy without crashing", %{
tls_server: tls_server,
proxy: proxy
} do
url = "#{tls_server.base_url}/redirect"
opts = [
pool: :media,
proxy: proxy.proxy_url,
insecure: true,
connect_timeout: 1_000,
recv_timeout: 1_000,
follow_redirect: true
]
assert {:ok, 200, _headers, ref} =
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
assert collect_body(ref) == "ok"
Pleroma.ReverseProxy.Client.Hackney.close(ref)
end
defp collect_body(ref, acc \\ "") do
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
:done -> acc
{:ok, data, _ref} -> collect_body(ref, acc <> data)
{:error, error} -> flunk("stream_body failed: #{inspect(error)}")
end
end
defp start_tls_redirect_server do
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
{:ok, listener} =
:ssl.listen(0, [
:binary,
certfile: certfile,
keyfile: keyfile,
reuseaddr: true,
active: false,
packet: :raw,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_tls_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
end
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
:ok = :ssl.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_tls_loop(listener) do
case :ssl.transport_accept(listener) do
{:ok, socket} ->
_ = Task.start(fn -> serve_tls(socket) end)
accept_tls_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve_tls(tcp_socket) do
with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000),
{:ok, data} <- recv_ssl_headers(ssl_socket),
{:ok, path} <- parse_path(data) do
case path do
"/redirect" ->
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
"/final" ->
send_ssl_response(ssl_socket, 200, "OK", [], "ok")
_ ->
send_ssl_response(ssl_socket, 404, "Not Found", [], "not found")
end
:ssl.close(ssl_socket)
else
_ ->
_ = :gen_tcp.close(tcp_socket)
:ok
end
end
defp recv_ssl_headers(socket, acc \\ <<>>) do
case :ssl.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
if :binary.match(acc, "\r\n\r\n") != :nomatch do
{:ok, acc}
else
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_ssl_headers(socket, acc)
end
end
{:error, _} = error ->
error
end
end
defp send_ssl_response(socket, status, reason, headers, body) do
base_headers =
[
{"Content-Length", Integer.to_string(byte_size(body))},
{"Connection", "close"}
] ++ headers
iodata =
[
"HTTP/1.1 ",
Integer.to_string(status),
" ",
reason,
"\r\n",
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
"\r\n",
body
]
:ssl.send(socket, iodata)
end
defp start_connect_proxy do
{:ok, listener} =
:gen_tcp.listen(0, [
:binary,
active: false,
packet: :raw,
reuseaddr: true,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_proxy_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}}
end
defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do
:ok = :gen_tcp.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_proxy_loop(listener) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
_ = Task.start(fn -> serve_proxy(socket) end)
accept_proxy_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve_proxy(client_socket) do
with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket),
{:ok, {host, port}} <- parse_connect(headers),
{:ok, upstream_socket} <- connect_upstream(host, port) do
:gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n")
if rest != <<>> do
:gen_tcp.send(upstream_socket, rest)
end
tunnel(client_socket, upstream_socket)
else
_ ->
:gen_tcp.close(client_socket)
:ok
end
end
defp tunnel(client_socket, upstream_socket) do
parent = self()
_ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end)
_ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end)
receive do
:tunnel_closed -> :ok
after
10_000 -> :ok
end
:gen_tcp.close(client_socket)
:gen_tcp.close(upstream_socket)
end
defp forward(from_socket, to_socket, parent) do
case :gen_tcp.recv(from_socket, 0, 10_000) do
{:ok, data} ->
_ = :gen_tcp.send(to_socket, data)
forward(from_socket, to_socket, parent)
{:error, _reason} ->
send(parent, :tunnel_closed)
:ok
end
end
defp recv_tcp_headers(socket, acc \\ <<>>) do
case :gen_tcp.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
case :binary.match(acc, "\r\n\r\n") do
:nomatch ->
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_tcp_headers(socket, acc)
end
{idx, _len} ->
split_at = idx + 4
<<headers::binary-size(split_at), rest::binary>> = acc
{:ok, {headers, rest}}
end
{:error, _} = error ->
error
end
end
defp parse_connect(data) do
with [request_line | _] <- String.split(data, "\r\n", trim: true),
["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3),
[host, port_str] <- String.split(hostport, ":", parts: 2),
{port, ""} <- Integer.parse(port_str) do
{:ok, {host, port}}
else
_ -> {:error, :invalid_connect}
end
end
defp connect_upstream(host, port) do
address =
case :inet.parse_address(String.to_charlist(host)) do
{:ok, ip} -> ip
{:error, _} -> String.to_charlist(host)
end
:gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000)
end
defp parse_path(data) do
case String.split(data, "\r\n", parts: 2) do
[request_line | _] ->
case String.split(request_line, " ") do
[_method, path, _protocol] -> {:ok, path}
_ -> {:error, :invalid_request}
end
_ ->
{:error, :invalid_request}
end
end
end

View file

@ -0,0 +1,151 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do
use ExUnit.Case, async: false
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
setup do
{:ok, _} = Application.ensure_all_started(:hackney)
{:ok, server} = start_server()
on_exit(fn -> stop_server(server) end)
{:ok, server: server}
end
test "pooled redirects work with follow_redirect disabled", %{server: server} do
url = "#{server.base_url}/redirect"
uri = URI.parse(url)
adapter_opts =
HackneyAdapterHelper.options(
[pool: :media, follow_redirect: false, no_proxy_env: true],
uri
)
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
end
defp start_server do
{:ok, listener} =
:gen_tcp.listen(0, [
:binary,
active: false,
packet: :raw,
reuseaddr: true,
ip: {127, 0, 0, 1}
])
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
{:ok, acceptor} =
Task.start_link(fn ->
accept_loop(listener)
end)
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
end
defp stop_server(%{listener: listener, acceptor: acceptor}) do
:ok = :gen_tcp.close(listener)
if Process.alive?(acceptor) do
Process.exit(acceptor, :normal)
end
end
defp accept_loop(listener) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
serve(socket)
accept_loop(listener)
{:error, :closed} ->
:ok
{:error, _reason} ->
:ok
end
end
defp serve(socket) do
with {:ok, data} <- recv_headers(socket),
{:ok, path} <- parse_path(data) do
case path do
"/redirect" ->
send_response(socket, 302, "Found", [{"Location", "/final"}], "")
"/final" ->
send_response(socket, 200, "OK", [], "ok")
_ ->
send_response(socket, 404, "Not Found", [], "not found")
end
else
_ -> :ok
end
:gen_tcp.close(socket)
end
defp recv_headers(socket, acc \\ <<>>) do
case :gen_tcp.recv(socket, 0, 1_000) do
{:ok, data} ->
acc = acc <> data
if :binary.match(acc, "\r\n\r\n") != :nomatch do
{:ok, acc}
else
if byte_size(acc) > 8_192 do
{:error, :too_large}
else
recv_headers(socket, acc)
end
end
{:error, _} = error ->
error
end
end
defp parse_path(data) do
case String.split(data, "\r\n", parts: 2) do
[request_line | _] ->
case String.split(request_line, " ") do
[_method, path, _protocol] -> {:ok, path}
_ -> {:error, :invalid_request}
end
_ ->
{:error, :invalid_request}
end
end
defp send_response(socket, status, reason, headers, body) do
base_headers =
[
{"Content-Length", Integer.to_string(byte_size(body))},
{"Connection", "close"}
] ++ headers
iodata =
[
"HTTP/1.1 ",
Integer.to_string(status),
" ",
reason,
"\r\n",
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
"\r\n",
body
]
:gen_tcp.send(socket, iodata)
end
end

View file

@ -54,14 +54,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
setup do: clear_config([:media_proxy, :enabled], true)
test "it prefetches media proxy URIs" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
with_mock HTTP,
get: fn _, _, opts ->
send(self(), {:prefetch_opts, opts})
{:ok, []}
end do
MediaProxyWarmingPolicy.filter(@message)
assert called(HTTP.get(:_, :_, :_))
assert_receive {:prefetch_opts, opts}
refute Keyword.has_key?(opts, :follow_redirect)
refute Keyword.has_key?(opts, :force_redirect)
end
end
@ -81,10 +84,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
end
test "history-aware" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history)
@ -93,10 +92,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
end
test "works with Updates" do
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
{:ok, %Tesla.Env{status: 200, body: ""}}
end)
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history |> Map.put("type", "Update"))

View file

@ -671,6 +671,19 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
end
describe "assign_report_to_account/2" do
test "assigns report to an account" do
reporter = insert(:user)
target_account = insert(:user)
%{id: assigned_id} = insert(:user)
{:ok, report} = CommonAPI.report(reporter, %{account_id: target_account.id})
{:ok, report} = Utils.assign_report_to_account(report, assigned_id)
assert %{data: %{"assigned_account" => ^assigned_id}} = report
end
end
describe "maybe_anonymize_reporter/1" do
setup do
reporter = insert(:user)

View file

@ -388,6 +388,38 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
|> json_response_and_validate_schema(:ok)
end
test "returns reports with specified assigned user", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, _report} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I don't like this user"
})
CommonAPI.assign_report_to_account(second_report_id, admin.id)
response =
conn
|> get(report_path(conn, :index, %{assigned_account: admin.id}))
|> json_response_and_validate_schema(:ok)
assert [open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == second_report_id
assert response["total"] == 1
end
test "renders content correctly", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
note = insert(:note, user: target_user, data: %{"content" => "mew 1"})
@ -467,6 +499,66 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
end
end
describe "POST /api/pleroma/admin/reports/assign_account" do
test "assigns account to report", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/reports/assign_account", %{
"reports" => [
%{"assigned_account" => admin.nickname, "id" => report_id}
]
})
|> json_response_and_validate_schema(:no_content)
activity = Activity.get_by_id_with_user_actor(report_id)
assert activity.data["assigned_account"] == admin.id
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} assigned report ##{report_id} (on user @#{activity.user_actor.nickname}) to user #{admin.nickname}"
end
test "unassigns account from report", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
CommonAPI.assign_report_to_account(report_id, admin.id)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/reports/assign_account", %{
"reports" => [
%{"assigned_account" => nil, "id" => report_id}
]
})
|> json_response_and_validate_schema(:no_content)
activity = Activity.get_by_id_with_user_actor(report_id)
assert activity.data["assigned_account"] == nil
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} unassigned report ##{report_id} (on user @#{activity.user_actor.nickname}) from a user"
end
end
describe "POST /api/pleroma/admin/reports/:id/notes" do
setup %{conn: conn, admin: admin} do
clear_config([:instance, :admin_privileges], [:reports_manage_reports])

View file

@ -36,6 +36,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
}),
AdminAPI.AccountView.render("show.json", %{user: other_user})
),
assigned_account: nil,
statuses: [],
notes: [],
state: "open",
@ -75,6 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
}),
AdminAPI.AccountView.render("show.json", %{user: other_user})
),
assigned_account: nil,
statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
notes: [],

View file

@ -1458,6 +1458,29 @@ defmodule Pleroma.Web.CommonAPITest do
}
} = flag_activity
end
test "assigns report to an account" do
[reporter, target_user] = insert_pair(:user)
%{id: assigned} = insert(:user)
{:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id})
{:ok, activity} = CommonAPI.assign_report_to_account(report_id, assigned)
assert %{data: %{"assigned_account" => ^assigned}} = activity
end
test "unassigns report from account" do
[reporter, target_user] = insert_pair(:user)
%{id: assigned} = insert(:user)
{:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{account_id: target_user.id})
CommonAPI.assign_report_to_account(report_id, assigned)
{:ok, activity} = CommonAPI.assign_report_to_account(report_id, nil)
refute Map.has_key?(activity.data, "assigned_account")
end
end
describe "reblog muting" do

View file

@ -1901,7 +1901,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _user_relationships} = User.mute(user, other_user1)
{:ok, _user_relationships} = User.mute(user, other_user2)
{:ok, _user_relationships} = User.mute(user, other_user3)
{:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
result =
conn
@ -1937,6 +1943,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}] = result
result =
conn
|> get("/api/v1/mutes")
|> json_response_and_validate_schema(200)
assert [
%{"id" => ^id3, "mute_expires_at" => ^date},
%{"id" => ^id2, "mute_expires_at" => nil},
%{"id" => ^id1, "mute_expires_at" => nil}
] = result
end
test "list of mutes with with_relationships parameter" do
@ -1951,20 +1968,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _} = User.mute(user, other_user1)
{:ok, _} = User.mute(user, other_user2)
{:ok, _} = User.mute(user, other_user3)
{:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
assert [
%{
"id" => ^id3,
"pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}}
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => ^date,
"followed_by" => true
}
}
},
%{
"id" => ^id2,
"pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}}
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => nil,
"followed_by" => true
}
}
},
%{
"id" => ^id1,
"pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}}
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => nil,
"followed_by" => true
}
}
}
] =
conn
@ -1980,7 +2021,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _user_relationship} = User.block(user, other_user1)
{:ok, _user_relationship} = User.block(user, other_user3)
{:ok, _user_relationship} = User.block(user, other_user2)
{:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
result =
conn
@ -2045,6 +2092,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] = result
result =
conn
|> assign(:user, user)
|> get("api/v1/blocks")
|> json_response_and_validate_schema(200)
assert [
%{"id" => ^id3, "block_expires_at" => nil},
%{"id" => ^id2, "block_expires_at" => ^date},
%{"id" => ^id1, "block_expires_at" => nil}
] = result
end
test "list of blocks with with_relationships parameter" do
@ -2059,20 +2118,44 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
{:ok, _} = User.block(user, other_user1)
{:ok, _} = User.block(user, other_user2)
{:ok, _} = User.block(user, other_user3)
{:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
assert [
%{
"id" => ^id3,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => ^date,
"followed_by" => false
}
}
},
%{
"id" => ^id2,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => nil,
"followed_by" => false
}
}
},
%{
"id" => ^id1,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => nil,
"followed_by" => false
}
}
}
] =
conn

View file

@ -54,8 +54,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f &#39;&amp;&lt;&gt;&quot;",
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_description: "",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_description: "",
header_static: "http://localhost:4001/images/banner.png",
emojis: [
%{
@ -326,8 +328,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
note: user.bio,
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_description: "",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_description: "",
header_static: "http://localhost:4001/images/banner.png",
emojis: [],
fields: [],
@ -439,8 +443,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
following: false,
followed_by: false,
blocking: false,
block_expires_at: nil,
blocked_by: false,
muting: false,
mute_expires_at: nil,
muting_notifications: false,
subscribing: false,
notifying: false,
@ -536,6 +542,53 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the blocking and blocked user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, user, other_user} = User.follow(user, other_user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60})
{:ok, _user_relationship} = User.block(other_user, user)
expected =
Map.merge(
@blank_response,
%{
following: false,
blocking: true,
block_expires_at: date,
blocked_by: true,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the muting user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, _user_relationship} =
User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60})
expected =
Map.merge(
@blank_response,
%{
muting: true,
mute_expires_at: date,
muting_notifications: true,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the user blocking a domain" do
user = insert(:user)
other_user = insert(:user, ap_id: "https://bad.site/users/other_user")
@ -856,12 +909,37 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60})
%{
mute_expires_at: mute_expires_at
} = AccountView.render("show.json", %{user: other_user, for: user, mutes: true})
pleroma: %{
relationship: %{
mute_expires_at: mute_expires_at
}
}
} = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true})
assert DateTime.diff(
mute_expires_at,
DateTime.utc_now() |> DateTime.add(24 * 60 * 60)
) in -3..3
end
test "renders block expiration date" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} =
User.block(user, other_user, %{duration: 24 * 60 * 60})
%{
pleroma: %{
relationship: %{
block_expires_at: block_expires_at
}
}
} = AccountView.render("show.json", %{user: other_user, for: user, embed_relationships: true})
assert DateTime.diff(
block_expires_at,
DateTime.utc_now() |> DateTime.add(24 * 60 * 60)
) in -3..3
end
end