Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
33e7ae7637 | |||
fa40db6b5a | |||
ccd8cd6c59 | |||
900b9b0124 | |||
43aef8b5b1 | |||
f1611b6292 | |||
c63ae73bc0 | |||
16d2bfef80 | |||
c8904f15a2 | |||
8fe29bf5d2 | |||
452f9e14fb | |||
5fa1cfc513 | |||
2aac70d690 | |||
3fa65a5c53 | |||
210df6fe92 | |||
8c956bc671 | |||
5144d6f4ba | |||
3e4a279a1b | |||
fc87baf1cf | |||
767e1272b3 | |||
07b478dc49 | |||
67cae52b08 | |||
4db42f5ab5 | |||
145191ef26 | |||
6674b33d75 | |||
2dfce40117 | |||
5e3ca133f2 | |||
3a13f91fff | |||
593ddbd796 | |||
1ae89bddcd | |||
5fe41df8aa | |||
5ce38591e5 | |||
2482d96782 | |||
f68b047bf7 | |||
48a0145736 | |||
d956dc2f09 | |||
40627a94d4 | |||
fb8081e1a3 | |||
1b560d547a | |||
0e5f55deea | |||
f0f0c76805 | |||
b3fc098b83 |
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added a new configuration option to the MediaProxy feature that allows the blocking of specific domains from using the media proxy or being explicitly allowed by the Content-Security-Policy.
|
||||||
|
- Please make sure instances you wanted to block media from are not in the MediaProxy `whitelist`, and instead use `blocklist`.
|
||||||
|
- `OnlyMedia` Upload Filter to simplify restricting uploads to audio, image, and video types
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Deactivated users can no longer show up in the emoji reaction list
|
||||||
|
- Embedded posts can no longer bypass `:restrict\_unauthenticated`
|
||||||
|
|
||||||
## 2023.05
|
## 2023.05
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
|
@ -443,7 +443,8 @@
|
||||||
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
|
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
|
||||||
max_read_duration: 30_000
|
max_read_duration: 30_000
|
||||||
],
|
],
|
||||||
whitelist: []
|
whitelist: [],
|
||||||
|
blocklist: []
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
|
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||||
method: :purge,
|
method: :purge,
|
||||||
|
|
|
@ -1558,7 +1558,21 @@
|
||||||
%{
|
%{
|
||||||
key: :whitelist,
|
key: :whitelist,
|
||||||
type: {:list, :string},
|
type: {:list, :string},
|
||||||
description: "List of hosts with scheme to bypass the MediaProxy",
|
description: """
|
||||||
|
List of hosts with scheme to bypass the MediaProxy.\n
|
||||||
|
The media will be fetched by the client, directly from the remote server.\n
|
||||||
|
To allow this, it will Content-Security-Policy exceptions for each instance listed.\n
|
||||||
|
This is to be used for instances you trust and do not want to cache media for.
|
||||||
|
""",
|
||||||
|
suggestions: ["http://example.com"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :blocklist,
|
||||||
|
type: {:list, :string},
|
||||||
|
description: """
|
||||||
|
List of hosts with scheme which will not go through the MediaProxy, and will not be explicitly allowed by the Content-Security-Policy.
|
||||||
|
This is to be used for instances where you do not want their media to go through your server or to be accessed by clients.
|
||||||
|
""",
|
||||||
suggestions: ["http://example.com"]
|
suggestions: ["http://example.com"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
docker-compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) akkoma
|
docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) akkoma
|
||||||
docker-compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) db
|
docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) db
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
docker-compose run --rm akkoma $@
|
docker compose run --rm akkoma $@
|
||||||
|
|
|
@ -42,7 +42,7 @@ For a frontend configured under the `available` key, it's enough to install it b
|
||||||
|
|
||||||
This will download the latest build for the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`).
|
This will download the latest build for the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`).
|
||||||
|
|
||||||
You can override any of the details. To install a Pleroma-FE build from a different URL, you could do this:
|
You can override any of the details. To install an Akkoma-FE build from a different URL, you could do this:
|
||||||
|
|
||||||
=== "OTP"
|
=== "OTP"
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,6 @@ mix ecto.migrate
|
||||||
# Start akkoma (replace with your system service manager's equivalent if different)
|
# Start akkoma (replace with your system service manager's equivalent if different)
|
||||||
sudo systemctl start akkoma
|
sudo systemctl start akkoma
|
||||||
|
|
||||||
# Update Pleroma-FE frontend to latest stable. For other Frontends see Frontend Configuration doc for more information.
|
# Update Akkoma-FE frontend to latest stable. For other Frontends see Frontend Configuration doc for more information.
|
||||||
mix pleroma.frontend install pleroma-fe --ref stable
|
mix pleroma.frontend install pleroma-fe --ref stable
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,6 +2,14 @@
|
||||||
Note: Additional clients may work, but these are known to work with Akkoma.
|
Note: Additional clients may work, but these are known to work with Akkoma.
|
||||||
Apps listed here might not support all of Akkoma's features.
|
Apps listed here might not support all of Akkoma's features.
|
||||||
|
|
||||||
|
## Multiplatform
|
||||||
|
### Kaiteki
|
||||||
|
- Homepage: <https://kaiteki.app/>
|
||||||
|
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
|
||||||
|
- Contact: [@kaiteki@fedi.software](https://fedi.software/@Kaiteki)
|
||||||
|
- Platforms: Web, Windows, Linux, Android
|
||||||
|
- Features: MastoAPI, Supports multiple backends
|
||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
### Whalebird
|
### Whalebird
|
||||||
- Homepage: <https://whalebird.social/>
|
- Homepage: <https://whalebird.social/>
|
||||||
|
|
|
@ -246,11 +246,11 @@ Notes:
|
||||||
|
|
||||||
### :frontend_configurations
|
### :frontend_configurations
|
||||||
|
|
||||||
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](https://docs-fe.akkoma.dev/stable/CONFIGURATION/#options).
|
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Akkoma-FE configuration and customization for instance administrators](https://docs-fe.akkoma.dev/stable/CONFIGURATION/#options).
|
||||||
|
|
||||||
Frontends can access these settings at `/api/v1/pleroma/frontend_configurations`
|
Frontends can access these settings at `/api/v1/pleroma/frontend_configurations`
|
||||||
|
|
||||||
To add your own configuration for Pleroma-FE, use it like this:
|
To add your own configuration for Akkoma-FE, use it like this:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
config :pleroma, :frontend_configurations,
|
config :pleroma, :frontend_configurations,
|
||||||
|
@ -615,6 +615,12 @@ This filter only strips the GPS and location metadata with Exiftool leaving colo
|
||||||
|
|
||||||
No specific configuration.
|
No specific configuration.
|
||||||
|
|
||||||
|
#### Pleroma.Upload.Filter.OnlyMedia
|
||||||
|
|
||||||
|
This filter rejects uploads that are not identified with Content-Type matching audio/\*, image/\*, or video/\*
|
||||||
|
|
||||||
|
No specific configuration.
|
||||||
|
|
||||||
#### Pleroma.Upload.Filter.Mogrify
|
#### Pleroma.Upload.Filter.Mogrify
|
||||||
|
|
||||||
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
|
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
|
||||||
|
|
|
@ -6,7 +6,7 @@ To add a custom theme to your instance, you'll first need to get a custom theme,
|
||||||
|
|
||||||
### Create your own theme
|
### Create your own theme
|
||||||
|
|
||||||
* You can create your own theme using the Pleroma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme.
|
* You can create your own theme using the Akkoma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme.
|
||||||
* To download your theme, you can do Save preset
|
* To download your theme, you can do Save preset
|
||||||
* If you want to upload a theme to customise it further, you can upload it using Load preset
|
* If you want to upload a theme to customise it further, you can upload it using Load preset
|
||||||
|
|
||||||
|
@ -70,4 +70,4 @@ config :pleroma, :frontend_configurations,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you added it in the back-end configuration file, you'll need to restart your instance for the changes to take effect. If you don't see the changes, it's probably because the browser has cached the previous theme. In that case you'll want to clear browser caches. Alternatively you can use a private/incognito window just to see the changes.
|
If you added it in the back-end configuration file, you'll need to restart your instance for the changes to take effect. If you don't see the changes, it's probably because the browser has cached the previous theme. In that case you'll want to clear browser caches. Alternatively you can use a private/incognito window just to see the changes.
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# Introduction to Akkoma
|
# Introduction to Akkoma
|
||||||
## What is Akkoma?
|
## What is Akkoma?
|
||||||
Akkoma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3.
|
Akkoma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3.
|
||||||
It actually consists of two components: a backend, named simply Akkoma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
|
It actually consists of two components: a backend, named simply Akkoma, and a user-facing frontend, named Akkoma-FE. It also includes the Mastodon frontend, if that's your thing.
|
||||||
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
|
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
|
||||||
One account on an instance is enough to talk to the entire fediverse!
|
One account on an instance is enough to talk to the entire fediverse!
|
||||||
|
|
||||||
|
@ -31,11 +31,11 @@ Installation instructions can be found in the installation section of these docs
|
||||||
## I got an account, now what?
|
## I got an account, now what?
|
||||||
Great! Now you can explore the fediverse! Open the login page for your Akkoma instance (e.g. <https://otp.akkoma.dev>) and login with your username and password. (If you don't have an account yet, click on Register)
|
Great! Now you can explore the fediverse! Open the login page for your Akkoma instance (e.g. <https://otp.akkoma.dev>) and login with your username and password. (If you don't have an account yet, click on Register)
|
||||||
|
|
||||||
### Pleroma-FE
|
### Akkoma-FE
|
||||||
The default front-end used by Akkoma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](https://docs-fe.akkoma.dev/stable/).
|
The default front-end used by Akkoma is Akkoma-FE. You can find more information on what it is and how to use it in the [Introduction to Akkoma-FE](https://docs-fe.akkoma.dev/stable/).
|
||||||
|
|
||||||
### Mastodon interface
|
### Mastodon interface
|
||||||
If the Pleroma-FE interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
|
If the Akkoma-FE interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
|
||||||
Just add a "/web" after your instance url (e.g. <https://otp.akkoma.dev/web>) and you'll end on the Mastodon web interface, but with a Akkoma backend! MAGIC!
|
Just add a "/web" after your instance url (e.g. <https://otp.akkoma.dev/web>) and you'll end on the Mastodon web interface, but with a Akkoma backend! MAGIC!
|
||||||
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
|
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
|
||||||
|
|
||||||
|
|
|
@ -23,23 +23,7 @@ sudo apt full-upgrade
|
||||||
sudo apt install git build-essential postgresql postgresql-contrib cmake libmagic-dev
|
sudo apt install git build-essential postgresql postgresql-contrib cmake libmagic-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Elixir and Erlang
|
### Create the akkoma user
|
||||||
|
|
||||||
* Install Elixir and Erlang (you might need to use backports or [asdf](https://github.com/asdf-vm/asdf) on old systems):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install elixir erlang-dev erlang-nox
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
|
|
||||||
|
|
||||||
```shell
|
|
||||||
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install AkkomaBE
|
|
||||||
|
|
||||||
* Add a new system user for the Akkoma service:
|
* Add a new system user for the Akkoma service:
|
||||||
|
|
||||||
|
@ -49,7 +33,67 @@ sudo useradd -r -s /bin/false -m -d /var/lib/akkoma -U akkoma
|
||||||
|
|
||||||
**Note**: To execute a single command as the Akkoma system user, use `sudo -Hu akkoma command`. You can also switch to a shell by using `sudo -Hu akkoma $SHELL`. If you don’t have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l akkoma -s $SHELL -c 'command'` and `su -l akkoma -s $SHELL` for starting a shell.
|
**Note**: To execute a single command as the Akkoma system user, use `sudo -Hu akkoma command`. You can also switch to a shell by using `sudo -Hu akkoma $SHELL`. If you don’t have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l akkoma -s $SHELL -c 'command'` and `su -l akkoma -s $SHELL` for starting a shell.
|
||||||
|
|
||||||
* Git clone the AkkomaBE repository from stable-branch and make the Akkoma user the owner of the directory:
|
### Install Elixir and Erlang
|
||||||
|
|
||||||
|
If your distribution packages a recent enough version of Elixir, you can install it directly from the distro repositories and skip to the next section of the guide:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt install elixir erlang-dev erlang-nox
|
||||||
|
```
|
||||||
|
|
||||||
|
Otherwise use [asdf](https://github.com/asdf-vm/asdf) to install the latest versions of Elixir and Erlang.
|
||||||
|
|
||||||
|
First, install some dependencies needed to build Elixir and Erlang:
|
||||||
|
```shell
|
||||||
|
sudo apt install curl unzip build-essential autoconf m4 libncurses5-dev libssh-dev unixodbc-dev xsltproc libxml2-utils libncurses-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then login to the `akkoma` user and install asdf:
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the following lines to `~/.bashrc`:
|
||||||
|
```shell
|
||||||
|
. "$HOME/.asdf/asdf.sh"
|
||||||
|
# asdf completions
|
||||||
|
. "$HOME/.asdf/completions/asdf.bash"
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the shell:
|
||||||
|
```shell
|
||||||
|
exec $SHELL
|
||||||
|
```
|
||||||
|
|
||||||
|
Next install Erlang:
|
||||||
|
```shell
|
||||||
|
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
|
||||||
|
export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"
|
||||||
|
asdf install erlang 25.3.2.1
|
||||||
|
asdf global erlang 25.3.2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Now install Elixir:
|
||||||
|
```shell
|
||||||
|
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
|
||||||
|
asdf install elixir 1.14.5-otp-25
|
||||||
|
asdf global elixir 1.14.5-otp-25
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm that Elixir is installed correctly by checking the version:
|
||||||
|
```shell
|
||||||
|
elixir --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install AkkomaBE
|
||||||
|
|
||||||
|
* Log into the `akkoma` user and clone the AkkomaBE repository from the stable branch and make the Akkoma user the owner of the directory:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo mkdir -p /opt/akkoma
|
sudo mkdir -p /opt/akkoma
|
||||||
|
|
|
@ -10,7 +10,7 @@ If you want to migrate from or OTP to docker, check out [the migration guide](./
|
||||||
|
|
||||||
### Prepare the system
|
### Prepare the system
|
||||||
|
|
||||||
* Install docker and docker-compose
|
* Install docker and docker compose
|
||||||
* [Docker](https://docs.docker.com/engine/install/)
|
* [Docker](https://docs.docker.com/engine/install/)
|
||||||
* [Docker-compose](https://docs.docker.com/compose/install/)
|
* [Docker-compose](https://docs.docker.com/compose/install/)
|
||||||
* This will usually just be a repository installation and a package manager invocation.
|
* This will usually just be a repository installation and a package manager invocation.
|
||||||
|
@ -26,7 +26,7 @@ echo "DOCKER_USER=$(id -u):$(id -g)" >> .env
|
||||||
```
|
```
|
||||||
|
|
||||||
This probably won't need to be changed, it's only there to set basic environment
|
This probably won't need to be changed, it's only there to set basic environment
|
||||||
variables for the docker-compose file.
|
variables for the docker compose file.
|
||||||
|
|
||||||
### Building the container
|
### Building the container
|
||||||
|
|
||||||
|
@ -65,9 +65,9 @@ cp config/generated_config.exs config/prod.secret.exs
|
||||||
We need to run a few commands on the database container, this isn't too bad
|
We need to run a few commands on the database container, this isn't too bad
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose run --rm --user akkoma -d db
|
docker compose run --rm --user akkoma -d db
|
||||||
# Note down the name it gives here, it will be something like akkoma_db_run
|
# Note down the name it gives here, it will be something like akkoma_db_run
|
||||||
docker-compose run --rm akkoma psql -h db -U akkoma -f config/setup_db.psql
|
docker compose run --rm akkoma psql -h db -U akkoma -f config/setup_db.psql
|
||||||
docker stop akkoma_db_run # Replace with the name you noted down
|
docker stop akkoma_db_run # Replace with the name you noted down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -84,17 +84,17 @@ We're going to run it in the foreground on the first run, just to make sure
|
||||||
everything start up.
|
everything start up.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
If everything went well, you should be able to access your instance at http://localhost:4000
|
If everything went well, you should be able to access your instance at http://localhost:4000
|
||||||
|
|
||||||
You can `ctrl-c` out of the docker-compose now to shutdown the server.
|
You can `ctrl-c` out of the docker compose now to shutdown the server.
|
||||||
|
|
||||||
### Running in the background
|
### Running in the background
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create your first user
|
### Create your first user
|
||||||
|
@ -125,8 +125,8 @@ cp docker-resources/Caddyfile.example docker-resources/Caddyfile
|
||||||
|
|
||||||
Then edit the TLD in your caddyfile to the domain you're serving on.
|
Then edit the TLD in your caddyfile to the domain you're serving on.
|
||||||
|
|
||||||
Uncomment the `caddy` section in the docker-compose file,
|
Uncomment the `caddy` section in the docker compose file,
|
||||||
then run `docker-compose up -d` again.
|
then run `docker compose up -d` again.
|
||||||
|
|
||||||
#### Running a reverse proxy on the host
|
#### Running a reverse proxy on the host
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ git pull
|
||||||
./docker-resources/manage.sh mix deps.get
|
./docker-resources/manage.sh mix deps.get
|
||||||
./docker-resources/manage.sh mix compile
|
./docker-resources/manage.sh mix compile
|
||||||
./docker-resources/manage.sh mix ecto.migrate
|
./docker-resources/manage.sh mix ecto.migrate
|
||||||
docker-compose restart akkoma db
|
docker compose restart akkoma db
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Further reading
|
#### Further reading
|
||||||
|
|
|
@ -117,4 +117,16 @@ To fix this, run:
|
||||||
mix pleroma.config delete pleroma frontends
|
mix pleroma.config delete pleroma frontends
|
||||||
```
|
```
|
||||||
|
|
||||||
which will remove the config from the database. Things should work now.
|
which will remove the config from the database. Things should work now.
|
||||||
|
|
||||||
|
## Migrating back to Pleroma
|
||||||
|
|
||||||
|
Akkoma is a hard fork of Pleroma. As such, migrating back is not guaranteed to always work. But if you want to migrate back to Pleroma, you can always try. Just note that you may run into unexpected issues and you're basically on your own. The following are some tips that may help, but note that these are barely tested, so proceed at your own risk.
|
||||||
|
|
||||||
|
First you will need to roll back the database migrations. The latest migration both Akkoma and Pleroma still have in common should be 20210416051708, so roll back to that. If you run from source, that should be
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MIX_ENV=prod mix ecto.rollback --to 20210416051708
|
||||||
|
```
|
||||||
|
|
||||||
|
Then switch back to Pleroma for updates (similar to how was done to migrate to Akkoma), and remove the front-ends. The front-ends are installed in the `frontends` folder in the [static directory](../configuration/static_dir.md). Once you are back to Pleroma, you will need to run the database migrations again. See the Pleroma documentation for this.
|
||||||
|
|
|
@ -10,7 +10,7 @@ You probably should, in the first instance.
|
||||||
|
|
||||||
### Prepare the system
|
### Prepare the system
|
||||||
|
|
||||||
* Install docker and docker-compose
|
* Install docker and docker compose
|
||||||
* [Docker](https://docs.docker.com/engine/install/)
|
* [Docker](https://docs.docker.com/engine/install/)
|
||||||
* [Docker-compose](https://docs.docker.com/compose/install/)
|
* [Docker-compose](https://docs.docker.com/compose/install/)
|
||||||
* This will usually just be a repository installation and a package manager invocation.
|
* This will usually just be a repository installation and a package manager invocation.
|
||||||
|
@ -46,7 +46,7 @@ For *most* from-source installs it'll already be there.
|
||||||
And the same with `uploads`, make sure your uploads (if you have them on disk) are
|
And the same with `uploads`, make sure your uploads (if you have them on disk) are
|
||||||
located at `uploads/` in the akkoma source directory.
|
located at `uploads/` in the akkoma source directory.
|
||||||
|
|
||||||
If you have them on a different disk, you will need to mount that disk into the docker-compose file,
|
If you have them on a different disk, you will need to mount that disk into the docker compose file,
|
||||||
with an entry that looks like this:
|
with an entry that looks like this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -66,7 +66,7 @@ echo "DOCKER_USER=$(id -u):$(id -g)" >> .env
|
||||||
```
|
```
|
||||||
|
|
||||||
This probably won't need to be changed, it's only there to set basic environment
|
This probably won't need to be changed, it's only there to set basic environment
|
||||||
variables for the docker-compose file.
|
variables for the docker compose file.
|
||||||
|
|
||||||
=== "From source"
|
=== "From source"
|
||||||
|
|
||||||
|
@ -126,21 +126,21 @@ mkdir pgdata
|
||||||
Now we can import our database to the container.
|
Now we can import our database to the container.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose run --rm --user akkoma -d db
|
docker compose run --rm --user akkoma -d db
|
||||||
docker-compose run --rm akkoma pg_restore -v -U akkoma -j $(grep -c ^processor /proc/cpuinfo) -d akkoma -h db akkoma_backup.sql
|
docker compose run --rm akkoma pg_restore -v -U akkoma -j $(grep -c ^processor /proc/cpuinfo) -d akkoma -h db akkoma_backup.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reverse proxies
|
### Reverse proxies
|
||||||
|
|
||||||
If you're just reusing your old proxy, you may have to uncomment the line in
|
If you're just reusing your old proxy, you may have to uncomment the line in
|
||||||
the docker-compose file under `ports`. You'll find it.
|
the docker compose file under `ports`. You'll find it.
|
||||||
|
|
||||||
Otherwise, you can use the same setup as the [docker installation guide](./docker_en.md#reverse-proxies).
|
Otherwise, you can use the same setup as the [docker installation guide](./docker_en.md#reverse-proxies).
|
||||||
|
|
||||||
### Let's go
|
### Let's go
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be at the same point as you were before, but with a docker install.
|
You should now be at the same point as you were before, but with a docker install.
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
# Optional software packages needed for specific functionality
|
# Optional software packages needed for specific functionality
|
||||||
|
|
||||||
For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required:
|
For specific Akkoma functionality (which is disabled by default) some or all of the below packages are required:
|
||||||
* `ImageMagic`
|
* `ImageMagick`
|
||||||
* `ffmpeg`
|
* `ffmpeg`
|
||||||
* `exiftool`
|
* `exiftool`
|
||||||
|
|
||||||
Please refer to documentation in `docs/installation` on how to install them on specific OS.
|
Please refer to documentation in `docs/installation` on how to install them on specific OS.
|
||||||
|
|
||||||
Note: the packages are not required with the current default settings of Pleroma.
|
Note: the packages are not required with the current default settings of Akkoma.
|
||||||
|
|
||||||
## `ImageMagick`
|
## `ImageMagick`
|
||||||
|
|
||||||
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
|
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
|
||||||
|
|
||||||
It is required for the following Pleroma features:
|
It is required for the following Akkoma features:
|
||||||
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
||||||
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||||
|
|
||||||
|
@ -21,12 +21,12 @@ It is required for the following Pleroma features:
|
||||||
|
|
||||||
`ffmpeg` is software to record, convert and stream audio and video.
|
`ffmpeg` is software to record, convert and stream audio and video.
|
||||||
|
|
||||||
It is required for the following Pleroma features:
|
It is required for the following Akkoma features:
|
||||||
* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||||
|
|
||||||
## `exiftool`
|
## `exiftool`
|
||||||
|
|
||||||
`exiftool` is media files metadata reader/writer.
|
`exiftool` is media files metadata reader/writer.
|
||||||
|
|
||||||
It is required for the following Pleroma features:
|
It is required for the following Akkoma features:
|
||||||
* `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
* `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
||||||
|
|
8
docs/theme/partials/source.html
vendored
8
docs/theme/partials/source.html
vendored
|
@ -38,11 +38,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page and page.url.startswith('backend') %}
|
{% if page and page.url.startswith('backend') %}
|
||||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %}
|
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma" %}
|
||||||
{% set repo_name = "pleroma/pleroma" %}
|
{% set repo_name = "AkkomaGang/akkoma" %}
|
||||||
{% elif page and page.url.startswith('frontend') %}
|
{% elif page and page.url.startswith('frontend') %}
|
||||||
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %}
|
{% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma-fe" %}
|
||||||
{% set repo_name = "pleroma/pleroma-fe" %}
|
{% set repo_name = "AkkomaGang/akkoma-fe" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set repo_url = config.repo_url %}
|
{% set repo_url = config.repo_url %}
|
||||||
{% set repo_name = config.repo_name %}
|
{% set repo_name = config.repo_name %}
|
||||||
|
|
|
@ -8,6 +8,8 @@ Restart=on-failure
|
||||||
|
|
||||||
; Uncomment this if you're on Arch Linux
|
; Uncomment this if you're on Arch Linux
|
||||||
; Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl"
|
; Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl"
|
||||||
|
; Uncomment if using asdf to manage Elixir and Erlang
|
||||||
|
; Environment="PATH=/var/lib/akkoma/.asdf/shims:/var/lib/akkoma/.asdf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
|
||||||
; Name of the user that runs the Akkoma service.
|
; Name of the user that runs the Akkoma service.
|
||||||
User=akkoma
|
User=akkoma
|
||||||
|
@ -24,6 +26,8 @@ Environment="HOME=/var/lib/akkoma"
|
||||||
WorkingDirectory=/opt/akkoma
|
WorkingDirectory=/opt/akkoma
|
||||||
; Path to the Mix binary.
|
; Path to the Mix binary.
|
||||||
ExecStart=/usr/bin/mix phx.server
|
ExecStart=/usr/bin/mix phx.server
|
||||||
|
; If using asdf comment the above line and uncomment the one below instead
|
||||||
|
; ExecStart=/var/lib/akkoma/.asdf/shims/mix phx.server
|
||||||
|
|
||||||
; Some security directives.
|
; Some security directives.
|
||||||
; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
|
; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
# 1. Replace 'example.tld' with your instance's domain wherever it appears.
|
# 1. Replace 'example.tld' with your instance's domain wherever it appears.
|
||||||
# 2. Copy this section into your Caddyfile and restart Caddy.
|
# 2. Copy this section into your Caddyfile and restart Caddy.
|
||||||
|
|
||||||
|
# If you are able to, it's highly recommended to have your media served via a separate subdomain for improved security.
|
||||||
|
# Uncomment the relevant sectons here and modify the base_url setting for Pleroma.Upload and :media_proxy accordingly.
|
||||||
|
|
||||||
example.tld {
|
example.tld {
|
||||||
log {
|
log {
|
||||||
output file /var/log/caddy/akkoma.log
|
output file /var/log/caddy/akkoma.log
|
||||||
|
@ -14,4 +17,21 @@ example.tld {
|
||||||
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
||||||
# and `localhost.` resolves to [::0] on some systems: see issue #930
|
# and `localhost.` resolves to [::0] on some systems: see issue #930
|
||||||
reverse_proxy 127.0.0.1:4000
|
reverse_proxy 127.0.0.1:4000
|
||||||
|
|
||||||
|
# Uncomment if using a separate media subdomain
|
||||||
|
#@mediaproxy path /media/* /proxy/*
|
||||||
|
#handle @mediaproxy {
|
||||||
|
# redir https://media.example.tld{uri} permanent
|
||||||
|
#}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Uncomment if using a separate media subdomain
|
||||||
|
#media.example.tld {
|
||||||
|
# @mediaproxy path /media/* /proxy/*
|
||||||
|
# reverse_proxy @mediaproxy 127.0.0.1:4000 {
|
||||||
|
# transport http {
|
||||||
|
# response_header_timeout 10s
|
||||||
|
# read_timeout 15s
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
|
|
@ -262,11 +262,14 @@ defp http_children do
|
||||||
proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
|
proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
|
||||||
pool_size = Config.get([:http, :pool_size])
|
pool_size = Config.get([:http, :pool_size])
|
||||||
|
|
||||||
|
:public_key.cacerts_load()
|
||||||
|
|
||||||
config =
|
config =
|
||||||
[:http, :adapter]
|
[:http, :adapter]
|
||||||
|> Config.get([])
|
|> Config.get([])
|
||||||
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|
||||||
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|
||||||
|
|> Pleroma.HTTP.AdapterHelper.maybe_add_cacerts(:public_key.cacerts_get())
|
||||||
|> Keyword.put(:name, MyFinch)
|
|> Keyword.put(:name, MyFinch)
|
||||||
|
|
||||||
[{Finch, config}]
|
[{Finch, config}]
|
||||||
|
|
|
@ -25,7 +25,7 @@ defmodule Pleroma.Constants do
|
||||||
|
|
||||||
const(static_only_files,
|
const(static_only_files,
|
||||||
do:
|
do:
|
||||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance embed sw.js sw-pleroma.js favicon.png schemas doc)
|
||||||
)
|
)
|
||||||
|
|
||||||
const(status_updatable_fields,
|
const(status_updatable_fields,
|
||||||
|
|
|
@ -47,6 +47,17 @@ def maybe_add_proxy_pool(opts, proxy) do
|
||||||
|> put_in([:pools, :default, :conn_opts, :proxy], proxy)
|
|> put_in([:pools, :default, :conn_opts, :proxy], proxy)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_add_cacerts(opts, nil), do: opts
|
||||||
|
|
||||||
|
def maybe_add_cacerts(opts, cacerts) do
|
||||||
|
opts
|
||||||
|
|> maybe_add_pools()
|
||||||
|
|> maybe_add_default_pool()
|
||||||
|
|> maybe_add_conn_opts()
|
||||||
|
|> maybe_add_transport_opts()
|
||||||
|
|> put_in([:pools, :default, :conn_opts, :transport_opts, :cacerts], cacerts)
|
||||||
|
end
|
||||||
|
|
||||||
def add_pool_size(opts, pool_size) do
|
def add_pool_size(opts, pool_size) do
|
||||||
opts
|
opts
|
||||||
|> maybe_add_pools()
|
|> maybe_add_pools()
|
||||||
|
@ -82,6 +93,16 @@ defp maybe_add_conn_opts(opts) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_add_transport_opts(opts) do
|
||||||
|
transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts])
|
||||||
|
|
||||||
|
unless is_nil(transport_opts) do
|
||||||
|
opts
|
||||||
|
else
|
||||||
|
put_in(opts, [:pools, :default, :conn_opts, :transport_opts], [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Merge default connection & adapter options with received ones.
|
Merge default connection & adapter options with received ones.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -38,9 +38,9 @@ def filter([filter | rest], upload) do
|
||||||
{:ok, :noop} ->
|
{:ok, :noop} ->
|
||||||
filter(rest, upload)
|
filter(rest, upload)
|
||||||
|
|
||||||
error ->
|
{:error, e} ->
|
||||||
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
|
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(e)}")
|
||||||
error
|
{:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
20
lib/pleroma/upload/filter/only_media.ex
Normal file
20
lib/pleroma/upload/filter/only_media.ex
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Upload.Filter.OnlyMedia do
|
||||||
|
@behaviour Pleroma.Upload.Filter
|
||||||
|
alias Pleroma.Upload
|
||||||
|
|
||||||
|
def filter(%Upload{content_type: content_type}) do
|
||||||
|
[type, _subtype] = String.split(content_type, "/")
|
||||||
|
|
||||||
|
if type in ["image", "video", "audio"] do
|
||||||
|
{:ok, :noop}
|
||||||
|
else
|
||||||
|
{:error, "Disallowed content-type: #{content_type}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter(_), do: {:ok, :noop}
|
||||||
|
end
|
|
@ -919,9 +919,13 @@ def add_attributed_to(object) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_attachments(object) do
|
def prepare_attachments(object) do
|
||||||
|
attachments = case Map.get(object, "attachment", []) do
|
||||||
|
[_ | _] = list -> list
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
|
||||||
attachments =
|
attachments =
|
||||||
object
|
attachments
|
||||||
|> Map.get("attachment", [])
|
|
||||||
|> Enum.map(fn data ->
|
|> Enum.map(fn data ->
|
||||||
[%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
|
[%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
|
||||||
|
|
||||||
|
|
|
@ -767,7 +767,7 @@ defp array_of_relationships do
|
||||||
"showing_reblogs" => true,
|
"showing_reblogs" => true,
|
||||||
"followed_by" => true,
|
"followed_by" => true,
|
||||||
"blocking" => false,
|
"blocking" => false,
|
||||||
"blocked_by" => true,
|
"blocked_by" => false,
|
||||||
"muting" => false,
|
"muting" => false,
|
||||||
"muting_notifications" => false,
|
"muting_notifications" => false,
|
||||||
"note" => "",
|
"note" => "",
|
||||||
|
@ -783,7 +783,7 @@ defp array_of_relationships do
|
||||||
"showing_reblogs" => true,
|
"showing_reblogs" => true,
|
||||||
"followed_by" => true,
|
"followed_by" => true,
|
||||||
"blocking" => false,
|
"blocking" => false,
|
||||||
"blocked_by" => true,
|
"blocked_by" => false,
|
||||||
"muting" => true,
|
"muting" => true,
|
||||||
"muting_notifications" => false,
|
"muting_notifications" => false,
|
||||||
"note" => "",
|
"note" => "",
|
||||||
|
|
|
@ -13,7 +13,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
|
||||||
description: "Relationship between current account and requested account",
|
description: "Relationship between current account and requested account",
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
blocked_by: %Schema{type: :boolean},
|
blocked_by: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Represents being blocked by this user. Always false."
|
||||||
|
},
|
||||||
blocking: %Schema{type: :boolean},
|
blocking: %Schema{type: :boolean},
|
||||||
domain_blocking: %Schema{type: :boolean},
|
domain_blocking: %Schema{type: :boolean},
|
||||||
endorsed: %Schema{type: :boolean},
|
endorsed: %Schema{type: :boolean},
|
||||||
|
|
|
@ -11,22 +11,31 @@ defmodule Pleroma.Web.EmbedController do
|
||||||
|
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
plug(:put_layout, :embed)
|
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do
|
def show(conn, %{"id" => id}) do
|
||||||
with %Activity{local: true} = activity <-
|
with {:activity, %Activity{} = activity} <-
|
||||||
Activity.get_by_id_with_object(id),
|
{:activity, Activity.get_by_id_with_object(id)},
|
||||||
true <- Visibility.is_public?(activity.object) do
|
{:local, true} <- {:local, activity.local},
|
||||||
|
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, nil)} do
|
||||||
{:ok, author} = User.get_or_fetch(activity.object.data["actor"])
|
{:ok, author} = User.get_or_fetch(activity.object.data["actor"])
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> delete_resp_header("x-frame-options")
|
|> delete_resp_header("x-frame-options")
|
||||||
|> delete_resp_header("content-security-policy")
|
|> delete_resp_header("content-security-policy")
|
||||||
|
|> put_view(Pleroma.Web.EmbedView)
|
||||||
|> render("show.html",
|
|> render("show.html",
|
||||||
activity: activity,
|
activity: activity,
|
||||||
author: User.sanitize_html(author),
|
author: User.sanitize_html(author),
|
||||||
counts: get_counts(activity)
|
counts: get_counts(activity)
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
{:activity, _} ->
|
||||||
|
render_error(conn, :not_found, "Post not found")
|
||||||
|
|
||||||
|
{:local, false} ->
|
||||||
|
render_error(conn, :unauthorized, "Federated posts cannot be embedded")
|
||||||
|
|
||||||
|
{:visible, false} ->
|
||||||
|
render_error(conn, :unauthorized, "Not authorized to view this post")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -124,14 +124,7 @@ def render(
|
||||||
target,
|
target,
|
||||||
&User.blocks_user?(&1, &2)
|
&User.blocks_user?(&1, &2)
|
||||||
),
|
),
|
||||||
blocked_by:
|
blocked_by: false,
|
||||||
UserRelationship.exists?(
|
|
||||||
user_relationships,
|
|
||||||
:block,
|
|
||||||
target,
|
|
||||||
reading_user,
|
|
||||||
&User.blocks_user?(&1, &2)
|
|
||||||
),
|
|
||||||
muting:
|
muting:
|
||||||
UserRelationship.exists?(
|
UserRelationship.exists?(
|
||||||
user_relationships,
|
user_relationships,
|
||||||
|
|
|
@ -52,7 +52,7 @@ def url(url) do
|
||||||
|
|
||||||
@spec url_proxiable?(String.t()) :: boolean()
|
@spec url_proxiable?(String.t()) :: boolean()
|
||||||
def url_proxiable?(url) do
|
def url_proxiable?(url) do
|
||||||
not local?(url) and not whitelisted?(url)
|
not local?(url) and not whitelisted?(url) and not blocked?(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_url(url, preview_params \\ []) do
|
def preview_url(url, preview_params \\ []) do
|
||||||
|
@ -83,6 +83,16 @@ def whitelisted?(url) do
|
||||||
domain in mediaproxy_whitelist_domains
|
domain in mediaproxy_whitelist_domains
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def blocked?(url) do
|
||||||
|
%{scheme: scheme, host: domain} = URI.parse(url)
|
||||||
|
# Block either the bare domain or the scheme-domain combo
|
||||||
|
scheme_domain = "#{scheme}://#{domain}"
|
||||||
|
blocklist = Config.get([:media_proxy, :blocklist])
|
||||||
|
|
||||||
|
Enum.member?(blocklist, domain) ||
|
||||||
|
Enum.member?(blocklist, scheme_domain)
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_get_domain_from_url("http" <> _ = url) do
|
defp maybe_get_domain_from_url("http" <> _ = url) do
|
||||||
URI.parse(url).host
|
URI.parse(url).host
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,6 +41,17 @@ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do
|
||||||
|
Enum.reject(ap_ids, fn ap_id ->
|
||||||
|
with false <- ap_id in excluded_ap_ids,
|
||||||
|
%{is_active: true} <- User.get_cached_by_ap_id(ap_id) do
|
||||||
|
false
|
||||||
|
else
|
||||||
|
_ -> true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
def filter_allowed_users(reactions, user, with_muted) do
|
def filter_allowed_users(reactions, user, with_muted) do
|
||||||
exclude_ap_ids =
|
exclude_ap_ids =
|
||||||
if is_nil(user) do
|
if is_nil(user) do
|
||||||
|
@ -51,7 +62,7 @@ def filter_allowed_users(reactions, user, with_muted) do
|
||||||
end
|
end
|
||||||
|
|
||||||
filter_emoji = fn emoji, users, url ->
|
filter_emoji = fn emoji, users, url ->
|
||||||
case Enum.reject(users, &(&1 in exclude_ap_ids)) do
|
case filter_allowed_user_by_ap_id(users, exclude_ap_ids) do
|
||||||
[] -> nil
|
[] -> nil
|
||||||
users -> {emoji, users, url}
|
users -> {emoji, users, url}
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||||
<meta content='noindex' name='robots'>
|
<meta content='noindex' name='robots'>
|
||||||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
<%= Phoenix.HTML.raw(assigns[:meta] || "") %>
|
||||||
<link rel="stylesheet" href="/embed.css">
|
<link rel="stylesheet" href="/embed/embed.css">
|
||||||
<base target="_parent">
|
<base target="_parent">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%= render @view_module, @view_template, assigns %>
|
<%= render view_module(@conn), view_template(@conn), assigns %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.EmbedView do
|
||||||
alias Pleroma.Web.Metadata.Utils
|
alias Pleroma.Web.Metadata.Utils
|
||||||
alias Pleroma.Web.Router.Helpers
|
alias Pleroma.Web.Router.Helpers
|
||||||
|
|
||||||
use Phoenix.HTML
|
import Phoenix.HTML
|
||||||
|
|
||||||
defdelegate full_nickname(user), to: User
|
defdelegate full_nickname(user), to: User
|
||||||
|
|
||||||
|
@ -55,10 +55,13 @@ defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) d
|
||||||
data["url"] || data["external_url"] || data["id"]
|
data["url"] || data["external_url"] || data["id"]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do
|
defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}})
|
||||||
|
when is_list(attachments) do
|
||||||
attachments
|
attachments
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp attachments(_), do: []
|
||||||
|
|
||||||
defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do
|
defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do
|
||||||
sensitive
|
sensitive
|
||||||
end
|
end
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -203,7 +203,8 @@ defp deps do
|
||||||
{:excoveralls, "0.15.1", only: :test},
|
{:excoveralls, "0.15.1", only: :test},
|
||||||
{:mox, "~> 1.0", only: :test},
|
{:mox, "~> 1.0", only: :test},
|
||||||
{:websockex, "~> 0.4.3", only: :test},
|
{:websockex, "~> 0.4.3", only: :test},
|
||||||
{:dialyxir, "~> 1.0", only: [:dev], runtime: false}
|
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
|
||||||
|
{:mint, "~> 1.5.1", override: true}
|
||||||
] ++ oauth_deps()
|
] ++ oauth_deps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
defmodule Pleroma.Repo.Migrations.ForcePinnedObjectsToExist do
|
defmodule Pleroma.Repo.Migrations.ForcePinnedObjectsToExist do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def up do
|
||||||
execute("UPDATE users SET pinned_objects = '{}' WHERE pinned_objects IS NULL")
|
execute("UPDATE users SET pinned_objects = '{}' WHERE pinned_objects IS NULL")
|
||||||
|
|
||||||
alter table("users") do
|
alter table("users") do
|
||||||
modify(:pinned_objects, :map, null: false, default: %{})
|
modify(:pinned_objects, :map, null: false, default: %{})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table("users") do
|
||||||
|
modify(:pinned_objects, :map, null: true, default: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
execute("UPDATE users SET pinned_objects = NULL WHERE pinned_objects = '{}'")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
defmodule Pleroma.Repo.Migrations.AddMastofeSettings do
|
defmodule Pleroma.Repo.Migrations.AddMastofeSettings do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def up do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add_if_not_exists(:mastofe_settings, :map)
|
add_if_not_exists(:mastofe_settings, :map)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:users) do
|
||||||
|
remove_if_exists(:mastofe_settings, :map)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
|
defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def up do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add_if_not_exists(:language, :string)
|
add_if_not_exists(:language, :string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:users) do
|
||||||
|
remove_if_exists(:language, :string)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
115
priv/static/embed/embed.css
Normal file
115
priv/static/embed/embed.css
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
body {
|
||||||
|
background-color: #282c37;
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar img {
|
||||||
|
float: left;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-content {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date a,
|
||||||
|
.counts {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counts dt,
|
||||||
|
.counts dd {
|
||||||
|
float: left;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-card {
|
||||||
|
min-height: 48px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-card a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
background-color: #419bdd;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #61a6d9;
|
||||||
|
}
|
43
priv/static/embed/embed.js
Normal file
43
priv/static/embed/embed.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
(function () {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var ready = function (loaded) {
|
||||||
|
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
|
||||||
|
loaded()
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(function () {
|
||||||
|
var iframes = []
|
||||||
|
|
||||||
|
window.addEventListener('message', function (e) {
|
||||||
|
var data = e.data || {}
|
||||||
|
|
||||||
|
if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iframes[data.id].height = data.height
|
||||||
|
});
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) {
|
||||||
|
iframe.scrolling = 'no'
|
||||||
|
iframe.style.overflow = 'hidden'
|
||||||
|
|
||||||
|
iframes.push(iframe)
|
||||||
|
|
||||||
|
var id = iframes.length - 1
|
||||||
|
|
||||||
|
iframe.onload = function () {
|
||||||
|
iframe.contentWindow.postMessage({
|
||||||
|
type: 'setHeightPleromaEmbed',
|
||||||
|
id: id
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.onload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
32
test/pleroma/upload/filter/only_media_test.exs
Normal file
32
test/pleroma/upload/filter/only_media_test.exs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Upload.Filter.OnlyMediaTest do
|
||||||
|
use Pleroma.DataCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.Upload
|
||||||
|
alias Pleroma.Upload.Filter.OnlyMedia
|
||||||
|
|
||||||
|
test "Allows media Content-Type" do
|
||||||
|
["audio/mpeg", "image/jpeg", "video/mp4"]
|
||||||
|
|> Enum.each(fn type ->
|
||||||
|
upload = %Upload{
|
||||||
|
content_type: type
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, :noop} = OnlyMedia.filter(upload)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Disallows non-media Content-Type" do
|
||||||
|
["application/javascript", "application/pdf", "text/html"]
|
||||||
|
|> Enum.each(fn type ->
|
||||||
|
upload = %Upload{
|
||||||
|
content_type: type
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:error, _} = OnlyMedia.filter(upload)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
44
test/pleroma/web/embed_controller_test.exs
Normal file
44
test/pleroma/web/embed_controller_test.exs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.EmbedControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "/embed", %{conn: conn} do
|
||||||
|
activity = insert(:note_activity)
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> get("/embed/#{activity.id}")
|
||||||
|
|> response(200)
|
||||||
|
|
||||||
|
object = Pleroma.Object.get_by_ap_id(activity.data["object"])
|
||||||
|
|
||||||
|
assert String.contains?(resp, object.data["content"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "/embed with a restricted post", %{conn: conn} do
|
||||||
|
activity = insert(:note_activity)
|
||||||
|
clear_config([:restrict_unauthenticated, :activities, :local], true)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/embed/#{activity.id}")
|
||||||
|
|> response(401)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "/embed with a private post", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
Pleroma.Web.CommonAPI.post(user, %{
|
||||||
|
status: "Mega ultra chicken status: #fried",
|
||||||
|
visibility: "private"
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/embed/#{activity.id}")
|
||||||
|
|> response(401)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1960,6 +1960,10 @@ test "index" do
|
||||||
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
|
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
|
||||||
User.mute(user, other_user)
|
User.mute(user, other_user)
|
||||||
|
|
||||||
|
deactivated_user = insert(:user)
|
||||||
|
{:ok, _} = CommonAPI.react_with_emoji(activity.id, deactivated_user, "🎅")
|
||||||
|
User.set_activation(deactivated_user, false)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
conn
|
conn
|
||||||
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|
||||||
|
@ -1967,6 +1971,7 @@ test "index" do
|
||||||
|
|
||||||
assert [
|
assert [
|
||||||
%{
|
%{
|
||||||
|
"emoji_reactions" => [],
|
||||||
"pleroma" => %{
|
"pleroma" => %{
|
||||||
"emoji_reactions" => []
|
"emoji_reactions" => []
|
||||||
}
|
}
|
||||||
|
|
|
@ -397,7 +397,22 @@ test "represent a relationship for the blocking and blocked user" do
|
||||||
expected =
|
expected =
|
||||||
Map.merge(
|
Map.merge(
|
||||||
@blank_response,
|
@blank_response,
|
||||||
%{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)}
|
%{following: false, blocking: true, blocked_by: false, id: to_string(other_user.id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
test_relationship_rendering(user, other_user, expected)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "blocks are not visible to the blocked user" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, _user_relationship} = User.block(other_user, user)
|
||||||
|
|
||||||
|
expected =
|
||||||
|
Map.merge(
|
||||||
|
@blank_response,
|
||||||
|
%{following: false, blocking: false, blocked_by: false, id: to_string(other_user.id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
test_relationship_rendering(user, other_user, expected)
|
test_relationship_rendering(user, other_user, expected)
|
||||||
|
|
|
@ -199,6 +199,15 @@ test "mediaproxy whitelist" do
|
||||||
assert unencoded == url
|
assert unencoded == url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "mediaproxy blocklist" do
|
||||||
|
clear_config([:media_proxy, :whitelist], ["https://google.com"])
|
||||||
|
clear_config([:media_proxy, :blocklist], ["https://feld.me"])
|
||||||
|
url = "https://feld.me/foo.png"
|
||||||
|
|
||||||
|
unencoded = MediaProxy.url(url)
|
||||||
|
assert unencoded == url
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: delete after removing support bare domains for media proxy whitelist
|
# TODO: delete after removing support bare domains for media proxy whitelist
|
||||||
test "mediaproxy whitelist bare domains whitelist (deprecated)" do
|
test "mediaproxy whitelist bare domains whitelist (deprecated)" do
|
||||||
clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"])
|
clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"])
|
||||||
|
@ -220,6 +229,18 @@ test "does not change whitelisted urls" do
|
||||||
assert String.starts_with?(encoded, media_url)
|
assert String.starts_with?(encoded, media_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "does not change blocked urls" do
|
||||||
|
clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"])
|
||||||
|
clear_config([:media_proxy, :base_url], "https://cache.pleroma.social")
|
||||||
|
|
||||||
|
media_url = "https://mycdn.akamai.com"
|
||||||
|
|
||||||
|
url = "#{media_url}/static/logo.png"
|
||||||
|
encoded = MediaProxy.url(url)
|
||||||
|
|
||||||
|
assert String.starts_with?(encoded, media_url)
|
||||||
|
end
|
||||||
|
|
||||||
test "ensure Pleroma.Upload base_url is always whitelisted" do
|
test "ensure Pleroma.Upload base_url is always whitelisted" do
|
||||||
media_url = "https://media.pleroma.social"
|
media_url = "https://media.pleroma.social"
|
||||||
clear_config([Pleroma.Upload, :base_url], media_url)
|
clear_config([Pleroma.Upload, :base_url], media_url)
|
||||||
|
|
|
@ -128,6 +128,12 @@ test "with media_proxy bare domains whitelist (deprecated)", %{conn: conn} do
|
||||||
clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"])
|
clear_config([:media_proxy, :whitelist], ["example4.com", "example5.com"])
|
||||||
assert_media_img_src(conn, "example5.com example4.com")
|
assert_media_img_src(conn, "example5.com example4.com")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "with media_proxy blocklist", %{conn: conn} do
|
||||||
|
clear_config([:media_proxy, :whitelist], ["https://example6.com", "https://example7.com"])
|
||||||
|
clear_config([:media_proxy, :blocklist], ["https://example8.com"])
|
||||||
|
assert_media_img_src(conn, "https://example7.com https://example6.com")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assert_media_img_src(conn, url) do
|
defp assert_media_img_src(conn, url) do
|
||||||
|
|
Loading…
Reference in a new issue