Merge branch 'develop' into tests/mastodon_api_controller.ex

This commit is contained in:
Maksim Pechnikov 2019-09-28 10:32:03 +03:00
commit 1053319cd6
56 changed files with 3986 additions and 2794 deletions

View file

@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Refreshing poll results for remote polls
- Admin API: Add ability to require password reset
- Pleroma API: `GET /api/v1/pleroma/subscription_notifications/` to get list of subscription notifications
- Pleroma API: `GET /api/v1/pleroma/subscription_notifications/:id` to get a subscription notification
- Pleroma API: `POST /api/v1/pleroma/subscription_notifications/clear` to clear all subscription notifications
- Pleroma API: `POST /api/v1/pleroma/subscription_notifications/dismiss` to clear a subscription notification
- Pleroma API: `DELETE /api/v1/pleroma/subscription_notifications/destroy_multiple` to clear multiple subscription notifications
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@ -15,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
- Mastodon API: notifications no longer include subscription notifications - they are now served from new endpoints in Pleroma API
### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
@ -45,7 +51,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Improve digest email template
Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
### Fixed
- Following from Osada
@ -108,6 +113,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
- Pleroma API: Email change endpoint.
- Admin API: Added moderation log
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- Web response cache (currently, enabled for ActivityPub)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
@ -119,6 +125,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.
## [1.0.7] - 2019-09-26
### Fixed
- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
### Changed
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
## [1.0.6] - 2019-08-14
### Fixed
- MRF: fix use of unserializable keyword lists in describe() implementations

View file

@ -591,6 +591,8 @@
config :pleroma, Pleroma.ActivityExpiration, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000

View file

@ -2687,6 +2687,42 @@
}
]
},
%{
group: :pleroma,
key: Pleroma.Plugs.RemoteIp,
type: :group,
description: """
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
""",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enable/disable the plug. Defaults to `false`.",
suggestions: [true, false]
},
%{
key: :headers,
type: {:list, :string},
description:
"A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`."
},
%{
key: :proxies,
type: {:list, :string},
description:
"A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`."
},
%{
key: :reserved,
type: {:list, :string},
description:
"Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)."
}
]
},
%{
group: :pleroma,
key: :web_cache_ttl,

View file

@ -711,6 +711,7 @@ Compile time settings (need instance reboot):
}
]
}
```
- Response:

View file

@ -423,6 +423,15 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack
## `POST /api/pleroma/emoji/packs/list_from`
### Requests the instance to list the packs from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* Response: JSON with the pack list, same as if the request was made to that instance's
list endpoint directly + 200 status
## `GET /api/pleroma/emoji/packs/:name/download_shared`
### Requests a local pack from the instance
* Method `GET`

View file

@ -730,6 +730,8 @@ This will probably take a long time.
This is an advanced feature and disabled by default.
If your instance is behind a reverse proxy you must enable and configure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip).
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
@ -756,3 +758,16 @@ Available caches:
* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
## Pleroma.Plugs.RemoteIp
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
Available options:
* `enabled` - Enable/disable the plug. Defaults to `false`.
* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`.
* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`.
* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network).

View file

@ -1,7 +1,9 @@
# Installing on Alpine Linux
## Installation
This guide is a step-by-step installation guide for Alpine Linux. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). 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 -l <username> -s $SHELL -c 'command'` instead.
This guide is a step-by-step installation guide for Alpine Linux. The instructions were verified against Alpine v3.10 standard image. You might miss additional dependencies if you use `netboot` instead.
It assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). 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 -l <username> -s $SHELL -c 'command'` instead.
### Required packages
@ -20,12 +22,13 @@ This guide is a step-by-step installation guide for Alpine Linux. It also assume
### Prepare the system
* First make sure to have the community repository enabled:
* The community repository must be enabled in `/etc/apk/repositories`. Depending on which version and mirror you use this looks like `http://alpine.42.fr/v3.10/community`. If you autogenerated the mirror during installation:
```shell
echo "https://nl.alpinelinux.org/alpine/latest-stable/community" | sudo tee -a /etc/apk/repository
awk 'NR==2' /etc/apk/repositories | sed 's/main/community/' | tee -a /etc/apk/repositories
```
* Then update the system, if not already done:
```shell
@ -77,7 +80,8 @@ sudo rc-update add postgresql
* Add a new system user for the Pleroma service:
```shell
sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma
sudo addgroup pleroma
sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma 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.
@ -164,7 +168,26 @@ If that doesnt work, make sure, that nginx is not already running. If it stil
sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf
```
* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name` and the paths to the certificates. You can use `nano` (install with `apk add nano` if missing).
```
server {
server_name your.domain;
listen 80;
...
}
server {
server_name your.domain;
listen 443 ssl http2;
...
ssl_trusted_certificate /etc/letsencrypt/live/your.domain/chain.pem;
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
...
}
```
* Enable and start nginx:
```shell

View file

@ -5,180 +5,179 @@
## インストール
このガイドはDebian Stretchを仮定しています。Ubuntu 16.04でも可能です
このガイドはDebian Stretchを利用することを想定しています。Ubuntu 16.04や18.04でもおそらく動作します。また、ユーザはrootもしくはsudoにより管理者権限を持っていることを前提とします。もし、以下の操作をrootユーザで行う場合は、 `sudo` を無視してください。ただし、`sudo -Hu pleroma` のようにユーザを指定している場合には `su <username> -s $SHELL -c 'command'` を代わりに使ってください
### 必要なソフトウェア
- PostgreSQL 9.6+ (postgresql-contrib-9.6 または他のバージョンの PSQL をインストールしてください)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like))。または [asdf](https://github.com/asdf-vm/asdf) を pleroma ユーザーでインストール。
- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
- postgresql-contrib 9.6以上 (同上)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- erlang-dev
- erlang-tools
- erlang-parsetools
- erlang-eldap (LDAP認証を有効化するときのみ必要)
- erlang-ssh
- erlang-xmerl (Jessieではバックポートからインストールすること)
- erlang-xmerl
- git
- build-essential
- openssh
- openssl
- nginx prefered (Apacheも動くかもしれませんが、誰もテストしていません)
- certbot (または何らかのACME Let's encryptクライアント)
#### このガイドで利用している追加パッケージ
- nginx (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください)
- certbot (または何らかのLet's Encrypt向けACMEクライアント)
### システムを準備する
* まずシステムをアップデートしてください。
```
apt update && apt dist-upgrade
sudo apt update
sudo apt full-upgrade
```
* 複数のツールとpostgresqlをインストールします。あとで必要になるので
* 上記に挙げたパッケージをインストールしておきます
```
apt install git build-essential openssl ssh sudo postgresql-9.6 postgresql-contrib-9.6
sudo apt install git build-essential postgresql postgresql-contrib
```
(postgresqlのバージョンは、あなたのディストロにあわせて変えてください。または、バージョン番号がいらないかもしれません。)
### ElixirとErlangをインストールします
* Erlangのリポジトリをダウンロードおよびインストールします。
```
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```
* ElixirとErlangをインストールします、
```
apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
```
### Pleroma BE (バックエンド) をインストールします
* 新しいユーザーを作ります。
```
adduser pleroma
```
(Give it any password you want, make it STRONG)
* Pleroma用に新しいユーザーを作ります。
* 新しいユーザーをsudoグループに入れます。
```
usermod -aG sudo pleroma
sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```
* 新しいユーザーに変身し、ホームディレクトリに移動します。
```
su pleroma
cd ~
```
**注意**: Pleromaユーザとして単発のコマンドを実行したい場合はは、`sudo -Hu pleroma command` を使ってください。シェルを使いたい場合は `sudo -Hu pleroma $SHELL`です。もし `sudo` を使わない場合は、rootユーザで `su -l pleroma -s $SHELL -c 'command'` とすることでコマンドを、`su -l pleroma -s $SHELL` とすることでシェルを開始できます。
* Gitリポジトリをクローンします。
```
git clone -b master https://git.pleroma.social/pleroma/pleroma
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* 新しいディレクトリに移動します。
```
cd pleroma/
cd /opt/pleroma
```
* Pleromaが依存するパッケージをインストールします。Hexをインストールしてもよいか聞かれたら、yesを入力してください。
```
mix deps.get
sudo -Hu pleroma mix deps.get
```
* コンフィギュレーションを生成します。
```
mix pleroma.instance gen
sudo -Hu pleroma mix pleroma.instance gen
```
* rebar3をインストールしてもよいか聞かれたら、yesを入力してください。
* この処理には時間がかかります。私もよく分かりませんが、何らかのコンパイルが行われているようです。
* あなたのインスタンスについて、いくつかの質問があります。その回答は `config/generated_config.exs` というコンフィギュレーションファイルに保存されます。
* このときにpleromaの一部がコンパイルされるため、この処理には時間がかかります。
* あなたのインスタンスについて、いくつかの質問されます。この質問により `config/generated_config.exs` という設定ファイルが生成されます。
**注意**: メディアプロクシを有効にすると回答して、なおかつ、キャッシュのURLは空欄のままにしている場合は、`generated_config.exs` を編集して、`base_url` で始まる行をコメントアウトまたは削除してください。そして、上にある行の `true` の後にあるコンマを消してください。
* コンフィギュレーションを確認して、もし問題なければ、ファイル名を変更してください。
```
mv config/{generated_config.exs,prod.secret.exs}
```
* これまでのコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。
* 先程のコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。
```
sudo su postgres -c 'psql -f config/setup_db.psql'
sudo -Hu pleroma mix pleroma.instance gen
```
* そして、データベースのグレーションを実行します。
* そして、データベースのマイグレーションを実行します。
```
MIX_ENV=prod mix ecto.migrate
sudo -Hu pleroma MIX_ENV=prod mix ecto.migrate
```
* Pleromaを起動できるようになりました。
* これでPleromaを起動できるようになりました。
```
MIX_ENV=prod mix phx.server
sudo -Hu pleroma MIX_ENV=prod mix phx.server
```
### インストールを終わらせる
### インストールの最終段階
あなたの新しいインスタンスを世界に向けて公開するには、nginxまたは何らかのウェブサーバー (プロクシ) を使用する必要があります。また、Pleroma のためにシステムサービスファイルを作成する必要があります。
あなたの新しいインスタンスを世界に向けて公開するには、nginx等のWebサーバやプロキシサーバをPleromaの前段に使用する必要があります。また、Pleroma のためにシステムサービスファイルを作成する必要があります。
#### Nginx
* まだインストールしていないなら、nginxをインストールします。
```
apt install nginx
sudo apt install nginx
```
* SSLをセットアップします。他の方法でもよいですが、ここではcertbotを説明します。
certbotを使うならば、まずそれをインストールします。
```
apt install certbot
sudo apt install certbot
```
そしてセットアップします。
```
mkdir -p /var/lib/letsencrypt/.well-known
% certbot certonly --email your@emailaddress --webroot -w /var/lib/letsencrypt/ -d yourdomain
sudo mkdir -p /var/lib/letsencrypt/
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
```
もしうまくいかないときは、先にnginxを設定してください。ssl "on" を "off" に変えてから再試行してください。
もしうまくいかないときは、nginxが正しく動いていない可能性があります。先にnginxを設定してください。ssl "on" を "off" に変えてから再試行してください。
---
* nginxコンフィギュレーションの例をnginxフォルダーにコピーします。
* nginxの設定ファイルサンプルをnginxフォルダーにコピーします。
```
cp /home/pleroma/pleroma/installation/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
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
```
* nginxを起動する前に、コンフィギュレーションを編集してください。例えば、サーバー名、証明書のパスなどを変更する必要があります。
* nginxを起動する前に、設定ファイルを編集してください。例えば、サーバー名、証明書のパスなどを変更する必要があります。
* nginxを再起動します。
```
systemctl reload nginx.service
sudo systemctl enable --now nginx.service
```
もし証明書を更新する必要が出てきた場合には、nginxの関連するlocationブロックのコメントアウトを外し、以下のコマンドを動かします。
```
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
```
#### 他のWebサーバやプロキシ
これに関してはサンプルが `/opt/pleroma/installation/` にあるので、探してみてください。
#### Systemd サービス
* サービスファイルの例をコピーします。
* サービスファイルのサンプルをコピーします。
```
cp /home/pleroma/pleroma/installation/pleroma.service /usr/lib/systemd/system/pleroma.service
sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
```
* サービスファイルを変更します。すべてのパスが正しいことを確認してください。また、`[Service]` セクションに以下の行があることを確認してください。
* サービスファイルを変更します。すべてのパスが正しいことを確認してください
* サービスを有効化し `pleroma.service` を開始してください
```
Environment="MIX_ENV=prod"
sudo systemctl enable --now pleroma.service
```
* `pleroma.service` を enable および start してください。
#### 初期ユーザの作成
新たにインスタンスを作成したら、以下のコマンドにより管理者権限を持った初期ユーザを作成できます。
```
systemctl enable --now pleroma.service
sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
```
#### モデレーターを作る
新たにユーザーを作ったら、モデレーター権限を与えたいかもしれません。以下のタスクで可能です。
```
mix set_moderator username [true|false]
```
モデレーターはすべてのポストを消すことができます。将来的には他のことも可能になるかもしれません。
#### メディアプロクシを有効にする
`generate_config` でメディアプロクシを有効にしているなら、すでにメディアプロクシが動作しています。あとから設定を変更したいなら、[How to activate mediaproxy](How-to-activate-mediaproxy) を見てください。
#### コンフィギュレーションとカスタマイズ
#### その他の設定とカスタマイズ
* [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html)

View file

@ -70,6 +70,7 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
# and `localhost.` resolves to [::0] on some systems: see issue #930

View file

@ -42,7 +42,7 @@ defp loop(state) do
end
def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity})
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HtmlSanitizeEx.strip_tags(status.content))
IO.puts("")

View file

@ -230,7 +230,6 @@ def get_notified_from_activity(
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)

View file

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RemoteIp do
@moduledoc """
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
"""
@behaviour Plug
@headers ~w[
forwarded
x-forwarded-for
x-client-ip
x-real-ip
]
# https://en.wikipedia.org/wiki/Localhost
# https://en.wikipedia.org/wiki/Private_network
@reserved ~w[
127.0.0.0/8
::1/128
fc00::/7
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
]
def init(_), do: nil
def call(conn, _) do
config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do
RemoteIp.call(conn, remote_ip_opts(config))
else
conn
end
end
defp remote_ip_opts(config) do
headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
reserved = Keyword.get(config, :reserved, @reserved)
proxies =
config
|> Keyword.get(:proxies, [])
|> Enum.concat(reserved)
|> Enum.map(&InetCidr.parse/1)
{headers, proxies}
end
end

View file

@ -0,0 +1,260 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SubscriptionNotification do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
@type t :: %__MODULE__{}
schema "subscription_notifications" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
def changeset(%SubscriptionNotification{} = notification, attrs) do
cast(notification, attrs, [])
end
def for_user_query(user, opts \\ []) do
query =
SubscriptionNotification
|> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
if opts[:with_muted] do
query
else
query
|> where([n, a], a.actor not in ^user.info.muted_notifications)
|> where([n, a], a.actor not in ^user.info.blocks)
|> where(
[n, a],
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
)
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
)
|> where([n, a, o, tm], is_nil(tm.user_id))
end
end
def for_user(user, opts \\ %{}) do
user
|> for_user_query(opts)
|> Pagination.fetch_paginated(opts)
end
@doc """
Returns notifications for user received since given date.
## Examples
iex> Pleroma.SubscriptionNotification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
[%Pleroma.SubscriptionNotification{}, %Pleroma.SubscriptionNotification{}]
iex> Pleroma.SubscriptionNotification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
[]
"""
@spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
user
|> for_user_query()
|> where([n], n.updated_at > ^date)
|> Repo.all()
end
def clear_up_to(%{id: user_id} = _user, id) do
from(
n in SubscriptionNotification,
where: n.user_id == ^user_id,
where: n.id <= ^id
)
|> Repo.delete_all([])
end
def get(%{id: user_id} = _user, id) do
query =
from(
n in SubscriptionNotification,
where: n.id == ^id,
join: activity in assoc(n, :activity),
preload: [activity: activity]
)
case Repo.one(query) do
%{user_id: ^user_id} = notification ->
{:ok, notification}
_ ->
{:error, "Cannot get notification"}
end
end
def clear(user) do
from(n in SubscriptionNotification, where: n.user_id == ^user.id)
|> Repo.delete_all()
end
def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in SubscriptionNotification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end
def dismiss(%{id: user_id} = _user, id) do
case Repo.get(SubscriptionNotification, id) do
%{user_id: ^user_id} = notification ->
Repo.delete(notification)
_ ->
{:error, "Cannot dismiss notification"}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
case Object.normalize(activity) do
%{data: %{"type" => "Answer"}} ->
{:ok, []}
_ ->
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Like", "Announce", "Follow"] do
notifications =
activity
|> get_notified_from_activity()
|> Enum.map(&create_notification(activity, &1))
{:ok, notifications}
end
def create_notifications(_), do: {:ok, []}
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless skip?(activity, user) do
notification = %SubscriptionNotification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Streamer.stream("user", notification)
Streamer.stream("user:subscription_notification", notification)
Push.send(notification)
notification
end
end
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
local_only
)
when type in ["Create", "Like", "Announce", "Follow"] do
[]
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
|> User.get_users_from_set(local_only)
end
def get_notified_from_activity(_, _local_only), do: []
@spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do
[
:self,
:followers,
:follows,
:non_followers,
:non_follows,
:recently_followed
]
|> Enum.any?(&skip?(&1, activity, user))
end
@spec skip?(atom(), Activity.t(), User.t()) :: boolean()
def skip?(:self, activity, user) do
activity.data["actor"] == user.ap_id
end
def skip?(
:followers,
%{data: %{"actor" => actor}},
%{info: %{notification_settings: %{"followers" => false}}} = user
) do
actor
|> User.get_cached_by_ap_id()
|> User.following?(user)
end
def skip?(
:non_followers,
activity,
%{info: %{notification_settings: %{"non_followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user)
end
def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(
:non_follows,
activity,
%{info: %{notification_settings: %{"non_follows" => false}}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed)
end
def skip?(:recently_followed, %{data: %{"type" => "Follow", "actor" => actor}}, user) do
user
|> SubscriptionNotification.for_user()
|> Enum.any?(&match?(%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}}, &1))
end
def skip?(_, _, _), do: false
end

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
@ -151,6 +152,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
Notification.create_notifications(activity)
SubscriptionNotification.create_notifications(activity)
participations =
activity

View file

@ -513,7 +513,7 @@ def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
|> render("show.json", %{activity: activity})
else
true ->
{:param_cast, nil}
@ -537,7 +537,7 @@ def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
|> render("show.json", %{activity: activity})
end
end

View file

@ -0,0 +1,219 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Pleroma.Web.Gettext
defstruct valid?: true,
errors: [],
user: nil,
params: %{},
status: nil,
summary: nil,
full_payload: nil,
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
visibility: nil,
expires_at: nil,
poll: nil,
emoji: %{},
content_html: nil,
mentions: [],
tags: [],
to: [],
cc: [],
context: nil,
sensitive: false,
object: nil,
preview?: false,
changes: %{}
def create(user, params) do
%__MODULE__{user: user}
|> put_params(params)
|> status()
|> summary()
|> with_valid(&attachments/1)
|> full_payload()
|> expires_at()
|> poll()
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
|> sensitive()
|> with_valid(&object/1)
|> preview?()
|> with_valid(&changes/1)
|> validate()
end
defp put_params(draft, params) do
params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
%__MODULE__{draft | params: params}
end
defp status(%{params: %{"status" => status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
%__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
end
defp full_payload(%{status: status, summary: summary} = draft) do
full_payload = String.trim(status <> summary)
case Utils.validate_character_limit(full_payload, draft.attachments) do
:ok -> %__MODULE__{draft | full_payload: full_payload}
{:error, message} -> add_error(draft, message)
end
end
defp attachments(%{params: params} = draft) do
attachments = Utils.attachments_from_ids(params)
%__MODULE__{draft | attachments: attachments}
end
defp in_reply_to(draft) do
case Map.get(draft.params, "in_reply_to_status_id") do
"" -> draft
nil -> draft
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
end
defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end
defp visibility(%{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct"))
{visibility, _} ->
%__MODULE__{draft | visibility: visibility}
end
end
defp expires_at(draft) do
case CommonAPI.check_expiry_date(draft.params["expires_in"]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
end
defp poll(draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
%__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
{:error, message} ->
add_error(draft, message)
end
end
defp content(draft) do
{content_html, mentions, tags} =
Utils.make_content_html(
draft.status,
draft.attachments,
draft.params,
draft.visibility
)
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
end
defp to_and_cc(draft) do
addressed_users =
draft.mentions
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|> Utils.get_addressed_users(draft.params["to"])
{to, cc} =
Utils.get_to_and_cc(
draft.user,
addressed_users,
draft.in_reply_to,
draft.visibility,
draft.in_reply_to_conversation
)
%__MODULE__{draft | to: to, cc: cc}
end
defp context(draft) do
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
%__MODULE__{draft | context: context}
end
defp sensitive(draft) do
sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
%__MODULE__{draft | sensitive: sensitive}
end
defp object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
object =
Utils.make_note_data(
draft.user.ap_id,
draft.to,
draft.context,
draft.content_html,
draft.attachments,
draft.in_reply_to,
draft.tags,
draft.summary,
draft.cc,
draft.sensitive,
draft.poll
)
|> Map.put("emoji", emoji)
%__MODULE__{draft | object: object}
end
defp preview?(draft) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false
%__MODULE__{draft | preview?: preview?}
end
defp changes(draft) do
direct? = draft.visibility == "direct"
changes =
%{
to: draft.to,
actor: draft.user,
context: draft.context,
object: draft.object,
additional: %{"cc" => draft.cc, "directMessage" => direct?}
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
%__MODULE__{draft | changes: changes}
end
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft
defp add_error(draft, message) do
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
end
defp validate(%{valid?: true} = draft), do: {:ok, draft}
defp validate(%{errors: [message | _]}), do: {:error, message}
end

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@ -18,14 +17,11 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
{:ok, follower, followed, activity}
end
end
@ -76,8 +72,7 @@ def delete(activity_id, user) do
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ ->
{:error, dgettext("errors", "Could not delete")}
_ -> {:error, dgettext("errors", "Could not delete")}
end
end
@ -87,18 +82,16 @@ def repeat(id_or_ap_id, user) do
nil <- Utils.get_existing_announce(user.ap_id, object) do
ActivityPub.announce(user, object)
else
_ ->
{:error, dgettext("errors", "Could not repeat")}
_ -> {:error, dgettext("errors", "Could not repeat")}
end
end
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
_ ->
{:error, dgettext("errors", "Could not unrepeat")}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
@ -108,30 +101,23 @@ def favorite(id_or_ap_id, user) do
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
_ ->
{:error, dgettext("errors", "Could not favorite")}
_ -> {:error, dgettext("errors", "Could not favorite")}
end
end
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
_ ->
{:error, dgettext("errors", "Could not unfavorite")}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
def vote(user, object, choices) do
with "Question" <- object.data["type"],
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
{options, max_count} <- get_options_and_max_count(object),
option_count <- Enum.count(options),
{:choice_check, {choices, true}} <-
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
@ -150,32 +136,40 @@ def vote(user, object, choices) do
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
defp validate_not_author(_, _), do: :ok
defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
{:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
{:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
{:error, dgettext("errors", "Already voted")}
end
end
defp get_options_and_max_count(object) do
if Map.has_key?(object.data, "anyOf") do
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{object.data["oneOf"], 1}
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end
defp normalize_and_validate_choice_indices(choices, count) do
Enum.map_reduce(choices, true, fn index, valid ->
index = if is_binary(index), do: String.to_integer(index), else: index
{index, if(valid, do: index < count, else: valid)}
end)
end
def get_visibility(_, _, %Participation{}) do
{"direct", "direct"}
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
@ -197,13 +191,13 @@ def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
Visibility.get_visibility(object)
end
end
defp check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, nil} = res), do: res
defp check_expiry_date({:ok, in_seconds}) do
def check_expiry_date({:ok, in_seconds}) do
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
if ActivityExpiration.expires_late_enough?(expiry) do
@ -213,107 +207,36 @@ defp check_expiry_date({:ok, in_seconds}) do
end
end
defp check_expiry_date(expiry_str) do
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
def post(user, %{"status" => status} = data) do
limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status),
attachments <- attachments_from_ids(data),
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
{visibility, in_reply_to_visibility} <-
get_visibility(data, in_reply_to, in_reply_to_conversation),
{_, false} <-
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
{content_html, mentions, tags} <-
make_content_html(
status,
attachments,
data,
visibility
),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <-
get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
context <- make_context(in_reply_to, in_reply_to_conversation),
cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
{:ok, expires_at} <- check_expiry_date(data["expires_in"]),
full_payload <- String.trim(status <> cw),
:ok <- validate_character_limit(full_payload, attachments, limit),
object <-
make_note_data(
user.ap_id,
to,
context,
content_html,
attachments,
in_reply_to,
tags,
cw,
cc,
sensitive,
poll
),
object <- put_emoji(object, full_payload, poll_emoji) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct"
result =
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => direct?}
}
|> maybe_add_list_data(user, visibility)
|> ActivityPub.create(preview?)
if expires_at do
with {:ok, activity} <- result do
{:ok, _} = ActivityExpiration.create(activity, expires_at)
def post(user, %{"status" => _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end
end
result
else
{:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")}
{:error, _} = e ->
e
e ->
{:error, e}
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end
# parse and put emoji to object data
defp put_emoji(map, text, emojis) do
Map.put(
map,
"emoji",
Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
)
end
defp maybe_create_activity_expiration(result, _), do: result
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji)
source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
user =
with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
user
else
_e -> user
case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
{:ok, user} -> user
_ -> user
end
ActivityPub.update(%{
@ -328,14 +251,8 @@ def update(user) do
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{
"type" => "Create"
},
object: %Object{
data: %{
"type" => "Note"
}
}
data: %{"type" => "Create"},
object: %Object{data: %{"type" => "Note"}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
@ -372,19 +289,13 @@ def remove_mute(user, activity) do
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
false
else
_ -> true
end
ThreadMute.check_muted(user.id, activity.data["context"]) != []
end
def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
def report(user, %{"account_id" => account_id} = data) do
with {:ok, account} <- get_reported_account(account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
@ -392,31 +303,32 @@ def report(user, data) do
statuses: statuses,
content: content_html,
forward: data["forward"] || false
}) do
{:ok, activity}
else
{:error, err} -> {:error, err}
{:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
{:account, nil} -> {:error, dgettext("errors", "Account not found")}
})
end
end
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id),
{:ok, activity} <- Utils.update_report_state(activity, state) do
{:ok, activity}
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts),
{:ok, activity} <- set_visibility(activity, opts) do
{:ok, activity}
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]
alias Calendar.Strftime
alias Pleroma.Activity
@ -41,14 +42,6 @@ def get_by_id_or_ap_id(id) do
end
end
def get_replied_to_activity(""), do: nil
def get_replied_to_activity(id) when not is_nil(id) do
Activity.get_by_id(id)
end
def get_replied_to_activity(_), do: nil
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end
@ -159,70 +152,74 @@ def maybe_add_list_data(activity_params, user, {:list, list_id}) do
def maybe_add_list_data(activity_params, _, _), do: activity_params
def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
when is_binary(expires_in) do
# In some cases mastofe sends out strings instead of integers
data
|> put_in(["poll", "expires_in"], String.to_integer(expires_in))
|> make_poll_data()
end
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do
%{max_expiration: max_expiration, min_expiration: min_expiration} =
limits = Pleroma.Config.get([:instance, :poll_limits])
# XXX: There is probably a cleaner way of doing this
try do
# In some cases mastofe sends out strings instead of integers
expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in
if Enum.count(options) > limits.max_options do
raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
end
{poll, emoji} =
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do
{option_notes, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
if String.length(option) > limits.max_option_chars do
raise ArgumentError,
message:
"Poll options cannot be longer than #{limits.max_option_chars} characters each"
end
{%{
note = %{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
}
{note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
end)
case expires_in do
expires_in when expires_in > max_expiration ->
raise ArgumentError, message: "Expiration date is too far in the future"
expires_in when expires_in < min_expiration ->
raise ArgumentError, message: "Expiration date is too soon"
_ ->
:noop
end
end_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.to_iso8601()
poll =
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
%{"type" => "Question", "anyOf" => poll, "closed" => end_time}
else
%{"type" => "Question", "oneOf" => poll, "closed" => end_time}
end
key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{poll, emoji}
rescue
e in ArgumentError -> e.message
{:ok, {poll, emoji}}
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
"Invalid poll"
{:error, "Invalid poll"}
end
def make_poll_data(_data) do
{%{}, %{}}
{:ok, {%{}, %{}}}
end
defp validate_poll_options_amount(options, %{max_options: max_options}) do
if Enum.count(options) > max_options do
{:error, "Poll can't contain more than #{max_options} options"}
else
:ok
end
end
defp validate_poll_options_length(options, %{max_option_chars: max_option_chars}) do
if Enum.any?(options, &(String.length(&1) > max_option_chars)) do
{:error, "Poll options cannot be longer than #{max_option_chars} characters each"}
else
:ok
end
end
defp validate_poll_expiration(expires_in, %{min_expiration: min, max_expiration: max}) do
cond do
expires_in > max -> {:error, "Expiration date is too far in the future"}
expires_in < min -> {:error, "Expiration date is too soon"}
true -> :ok
end
end
def make_content_html(
@ -234,7 +231,7 @@ def make_content_html(
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|> Kernel.in([true, "true"])
|> truthy_param?()
content_type = get_content_type(data["content_type"])
@ -347,25 +344,25 @@ def make_note_data(
attachments,
in_reply_to,
tags,
cw \\ nil,
summary \\ nil,
cc \\ [],
sensitive \\ false,
merge \\ %{}
extra_params \\ %{}
) do
%{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
"summary" => cw,
"sensitive" => !Enum.member?(["false", "False", "0", false], sensitive),
"summary" => summary,
"sensitive" => truthy_param?(sensitive),
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => Keyword.values(tags) |> Enum.uniq()
}
|> add_in_reply_to(in_reply_to)
|> Map.merge(merge)
|> Map.merge(extra_params)
end
defp add_in_reply_to(object, nil), do: object
@ -434,12 +431,14 @@ def confirm_current_password(user, password) do
end
end
def emoji_from_profile(%{info: _info} = user) do
(Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, %Emoji{file: url}} ->
def emoji_from_profile(%User{bio: bio, name: name}) do
[bio, name]
|> Enum.map(&Emoji.Formatter.get_emoji/1)
|> Enum.concat()
|> Enum.map(fn {shortcode, %Emoji{file: path}} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
"name" => ":#{shortcode}:"
}
end)
@ -571,15 +570,16 @@ def make_answer_data(%User{ap_id: ap_id}, object, name) do
}
end
def validate_character_limit(full_payload, attachments, limit) do
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
def validate_character_limit(full_payload, _attachments) do
limit = Pleroma.Config.get([:instance, :limit])
length = String.length(full_payload)
if length < limit do
if length > 0 or Enum.count(attachments) > 0 do
:ok
else
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
else
{:error, dgettext("errors", "The status is over the character limit")}
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
@falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values

View file

@ -97,10 +97,7 @@ defmodule Pleroma.Web.Endpoint do
extra: extra
)
# Note: the plug and its configuration is compile-time this can't be upstreamed yet
if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
plug(RemoteIp, proxies: proxies)
end
plug(Pleroma.Plugs.RemoteIp)
defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter

View file

@ -0,0 +1,26 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
use Pleroma.Web, :controller
alias Pleroma.User
@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, Map.get(info, :domain_blocks, []))
end
@doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
end

View file

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterController do
use Pleroma.Web, :controller
alias Pleroma.Filter
@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
render(conn, "filters.json", filters: filters)
end
@doc "POST /api/v1/filters"
def create(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
query = %Filter{
user_id: user.id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Filter.create(query)
render(conn, "filter.json", filter: response)
end
@doc "GET /api/v1/filters/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
filter = Filter.get(filter_id, user)
render(conn, "filter.json", filter: filter)
end
@doc "PUT /api/v1/filters/:id"
def update(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
query = %Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", nil),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Filter.update(query)
render(conn, "filter.json", filter: response)
end
@doc "DELETE /api/v1/filters/:id"
def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
}
{:ok, _} = Filter.delete(query)
json(conn, %{})
end
end

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.CommonAPI
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)
render(conn, "accounts.json", for: followed, users: follow_requests, as: :user)
end
@doc "POST /api/v1/follow_requests/:id/authorize"
def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
render(conn, "relationship.json", user: followed, target: follower)
end
end
@doc "POST /api/v1/follow_requests/:id/reject"
def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
render(conn, "relationship.json", user: followed, target: follower)
end
end
defp assign_follower(%{params: %{"id" => id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
defp errors(conn, {:error, message}) do
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
alias Ecto.Changeset
alias Pleroma.Activity
@ -14,13 +14,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Filter
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@ -30,51 +28,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.RichMedia
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.ControllerHelper
import Ecto.Query
require Logger
require Pleroma.Constants
@rate_limited_relations_actions ~w(follow unfollow)a
@rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
post_status delete_status)a
plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
when action in ~w(reblog_status unreblog_status)a
)
plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
when action in ~w(fav_status unfav_status)a
)
plug(
RateLimiter,
{:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
)
plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
plug(RateLimiter, :app_account_creation when action == :account_register)
plug(RateLimiter, :search when action in [:search, :search2, :account_search])
plug(RateLimiter, :password_reset when action == :password_reset)
@ -157,7 +133,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)}
{:ok, truthy_param?(value)}
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
@ -345,43 +321,6 @@ def custom_emojis(conn, _params) do
json(conn, mastodon_emoji)
end
def home_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
|> add_link_headers(activities, %{"local" => local_only})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
params =
@ -401,80 +340,6 @@ def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
end
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
limit = 100
activities =
ids
|> Enum.take(limit)
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
conn
|> put_view(StatusView)
|> render("index.json", activities: activities, for: user, as: :activity)
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
end
end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
}),
grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
result = %{
ancestors:
StatusView.render("index.json",
for: user,
activities: grouped_activities[true] || [],
as: :activity
)
|> Enum.reverse(),
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
descendants:
StatusView.render("index.json",
for: user,
activities: grouped_activities[false] || [],
as: :activity
)
|> Enum.reverse()
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
}
json(conn, result)
end
end
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
@ -525,193 +390,6 @@ def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choic
end
end
def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
|> put_view(ScheduledActivityView)
|> render("index.json", %{scheduled_activities: scheduled_activities})
end
end
def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
_ -> {:error, :not_found}
end
end
def update_scheduled_status(
%{assigns: %{user: user}} = conn,
%{"id" => scheduled_activity_id} = params
) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id),
{:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end
end
def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.get(user, scheduled_activity_id),
{:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end
end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params =
params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
scheduled_at = params["scheduled_at"]
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
with {:ok, scheduled_activity} <-
ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
end
else
params = Map.drop(params, ["scheduled_at"])
case CommonAPI.post(user, params) do
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:ok, activity} ->
conn
|> put_view(StatusView)
|> try_render("status.json", %{
activity: activity,
for: user,
as: :activity,
with_direct_conversation_id: true
})
end
end
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e -> render_error(conn, :forbidden, "Can't delete this post")
end
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
activity = Activity.get_by_id(id)
with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
activity = Activity.get_by_id(id)
with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
targets = User.get_all_by_ids(List.wrap(id))
@ -778,83 +456,6 @@ def get_mascot(%{assigns: %{user: user}} = conn, _params) do
json(conn, mascot)
end
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
q = from(u in User, where: u.ap_id in ^likes)
users =
Repo.all(q)
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("accounts.json", %{for: user, users: users, as: :user})
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
q = from(u in User, where: u.ap_id in ^announces)
users =
Repo.all(q)
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("accounts.json", %{for: user, users: users, as: :user})
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))
tag_all =
params["all"] ||
[]
|> Enum.map(&String.downcase(&1))
tag_reject =
params["none"] ||
[]
|> Enum.map(&String.downcase(&1))
activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
|> add_link_headers(activities, %{"local" => local_only})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_followers(user, params) do
@ -889,42 +490,6 @@ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
end
end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)
conn
|> put_view(AccountView)
|> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
@ -1054,20 +619,6 @@ def blocks(%{assigns: %{user: user}} = conn, _) do
end
end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info.domain_blocks || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %User{} = subscription_target <- User.get_cached_by_id(id),
{:ok, subscription_target} = User.subscribe(user, subscription_target) do
@ -1165,31 +716,6 @@ def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
|> render("index.json", %{lists: lists})
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("muting_user", user)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
activities =
following
|> Enum.filter(fn x -> x in user.following end)
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
else
_e -> render_error(conn, :forbidden, "Error.")
end
end
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
@ -1368,62 +894,8 @@ def empty_array(conn, _) do
json(conn, [])
end
def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
json(conn, res)
end
def create_filter(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
query = %Filter{
user_id: user.id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Filter.create(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
filter = Filter.get(filter_id, user)
res = FilterView.render("filter.json", filter: filter)
json(conn, res)
end
def update_filter(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
query = %Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", nil),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Filter.update(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
}
{:ok, _} = Filter.delete(query)
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
end
@ -1474,21 +946,6 @@ defp fetch_suggestion_id(attrs) do
end
end
@doc false
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
def status_card(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, user) do
data = RichMedia.Helpers.fetch_data_for_activity(activity)
conn
|> put_view(StatusView)
|> render("card.json", data)
else
_e -> {:error, :not_found}
end
end
def reports(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.report(user, params) do
{:ok, activity} ->
@ -1597,7 +1054,7 @@ def account_confirmation_resend(conn, params) do
end
end
defp try_render(conn, target, params)
def try_render(conn, target, params)
when is_binary(target) do
case render(conn, target, params) do
nil -> render_error(conn, :not_implemented, "Can't display this activity")
@ -1605,7 +1062,7 @@ defp try_render(conn, target, params)
end
end
defp try_render(conn, _, _) do
def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity")
end

View file

@ -0,0 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI
plug(:assign_scheduled_activity when action != :index)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
|> render("index.json", scheduled_activities: scheduled_activities)
end
end
@doc "GET /api/v1/scheduled_statuses/:id"
def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@doc "PUT /api/v1/scheduled_statuses/:id"
def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end
@doc "DELETE /api/v1/scheduled_statuses/:id"
def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end
defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
end

View file

@ -0,0 +1,274 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
require Ecto.Query
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
when action in ~w(favourite unfavourite)a
)
plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
limit = 100
activities =
ids
|> Enum.take(limit)
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
render(conn, "index.json", activities: activities, for: user, as: :activity)
end
@doc """
POST /api/v1/statuses
Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
%{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params
) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
if ScheduledActivity.far_enough?(scheduled_at) do
with {:ok, scheduled_activity} <-
ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
end
else
create(conn, Map.drop(params, ["scheduled_at"]))
end
end
@doc """
POST /api/v1/statuses
Creates a regular status
"""
def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
activity: activity,
for: user,
as: :activity,
with_direct_conversation_id: true
)
else
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end
def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
create(conn, Map.put(params, "status", ""))
end
@doc "GET /api/v1/statuses/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json", activity: activity, for: user)
end
end
@doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e -> render_error(conn, :forbidden, "Can't delete this post")
end
end
@doc "POST /api/v1/statuses/:id/reblog"
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unfavourite"
def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/pin"
def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unpin"
def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unbookmark"
def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/mute"
def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unmute"
def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
render(conn, "card.json", data)
else
_ -> render_error(conn, :not_found, "Record not found")
end
end
@doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("accounts.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("accounts.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/context"
def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
})
render(conn, "context.json", activity: activity, activities: activities, user: user)
end
end
end

View file

@ -0,0 +1,136 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
alias Pleroma.Pagination
alias Pleroma.Web.ActivityPub.ActivityPub
plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
recipients = [user.ap_id | user.following]
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end
# GET /api/v1/timelines/direct
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
local_only = truthy_param?(params["local"])
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
end
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = truthy_param?(params["local"])
tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))
tag_all =
params
|> Map.get("all", [])
|> Enum.map(&String.downcase(&1))
tag_reject =
params
|> Map.get("none", [])
|> Enum.map(&String.downcase(&1))
activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
end
# GET /api/v1/timelines/list/:list_id
def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("muting_user", user)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
activities =
following
|> Enum.filter(fn x -> x in user.following end)
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
render(conn, "index.json", activities: activities, for: user, as: :activity)
else
_e -> render_error(conn, :forbidden, "Error.")
end
end
end

View file

@ -24,7 +24,7 @@ def render("participation.json", %{participation: participation, for: user}) do
activity = Activity.get_by_id_with_object(last_activity_id)
last_status = StatusView.render("status.json", %{activity: activity, for: user})
last_status = StatusView.render("show.json", %{activity: activity, for: user})
# Conversations return all users except the current user.
users =

View file

@ -39,19 +39,19 @@ def render("show.json", %{
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
status: StatusView.render("show.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"follow" ->

View file

@ -7,11 +7,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{scheduled_activities: scheduled_activities}) do
render_many(scheduled_activities, ScheduledActivityView, "show.json")
render_many(scheduled_activities, __MODULE__, "show.json")
end
def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
@ -24,12 +23,8 @@ def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_a
end
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
try do
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
Map.put(data, :media_attachments, attachments)
rescue
_ -> data
end
end
defp with_media_attachments(data, _), do: data
@ -45,13 +40,9 @@ defp status_params(params) do
in_reply_to_id: params["in_reply_to_id"]
}
data =
if media_ids = params["media_ids"] do
Map.put(data, :media_ids, media_ids)
else
data
end
data
case params["media_ids"] do
nil -> data
media_ids -> Map.put(data, :media_ids, media_ids)
end
end
end

View file

@ -73,17 +73,13 @@ defp reblogged?(activity, user) do
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
opts = Map.put(opts, :replied_to_activities, replied_to_activities)
opts.activities
|> safe_render_many(
StatusView,
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
safe_render_many(opts.activities, StatusView, "show.json", opts)
end
def render(
"status.json",
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
@ -96,7 +92,7 @@ def render(
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
@ -144,7 +140,7 @@ def render(
}
end
def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
@ -303,7 +299,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
}
end
def render("status.json", _) do
def render("show.json", _) do
nil
end
@ -441,6 +437,20 @@ def render("poll.json", %{object: object} = opts) do
end
end
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])
%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)

View file

@ -0,0 +1,71 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Activity
alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.PleromaAPI.PleromaAPI
def index(%{assigns: %{user: user}} = conn, params) do
notifications =
user
|> PleromaAPI.get_subscription_notifications(params)
|> Enum.map(&build_notification_data/1)
conn
|> add_link_headers(notifications)
|> render("index.json", %{notifications: notifications, for: user})
end
def show(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- SubscriptionNotification.get(user, id) do
render(conn, "show.json", %{
subscription_notification: build_notification_data(notification),
for: user
})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def clear(%{assigns: %{user: user}} = conn, _params) do
SubscriptionNotification.clear(user)
json(conn, %{})
end
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- SubscriptionNotification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end
def destroy_multiple(
%{assigns: %{user: user}} = conn,
%{"ids" => ids} = _params
) do
SubscriptionNotification.destroy_multiple(user, ids)
json(conn, %{})
end
defp build_notification_data(%{activity: %{data: data}} = notification) do
%{
notification: notification,
actor: User.get_cached_by_ap_id(data["actor"]),
parent_activity: Activity.get_create_by_object_ap_id(data["object"])
}
end
end

View file

@ -0,0 +1,40 @@
defmodule Pleroma.Web.PleromaAPI.PleromaAPI do
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Pagination
alias Pleroma.SubscriptionNotification
def get_subscription_notifications(user, params \\ %{}) do
options = cast_params(params)
user
|> SubscriptionNotification.for_user_query(options)
|> restrict(:exclude_types, options)
|> Pagination.fetch_paginated(params)
end
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
reblogs: :boolean,
with_muted: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
ap_types =
mastodon_types
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|> Enum.filter(& &1)
query
|> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end
defp restrict(query, _, _), do: query
end

View file

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.PleromaAPI.SubscriptionNotificationView
def render("index.json", %{notifications: notifications, for: user}) do
safe_render_many(notifications, SubscriptionNotificationView, "show.json", %{for: user})
end
def render("show.json", %{
subscription_notification: %{
notification: %{activity: activity} = notification,
actor: actor,
parent_activity: parent_activity
},
for: user
}) do
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: AccountView.render("account.json", %{user: actor, for: user})
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.Push.Impl do
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Push.Subscription
@ -19,7 +20,7 @@ defmodule Pleroma.Web.Push.Impl do
@types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error
@spec perform(Notification.t() | SubscriptionNotification.t()) :: list(any) | :error
def perform(
%{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,

View file

@ -293,6 +293,14 @@ defmodule Pleroma.Web.Router do
pipe_through(:oauth_read)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
scope "/subscription_notifications" do
post("/clear", SubscriptionNotificationController, :clear)
post("/dismiss", SubscriptionNotificationController, :dismiss)
delete("/destroy_multiple", SubscriptionNotificationController, :destroy_multiple)
get("/", SubscriptionNotificationController, :index)
get("/:id", SubscriptionNotificationController, :show)
end
end
scope [] do
@ -315,12 +323,12 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", MastodonAPIController, :follow_requests)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :mutes)
get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/favourites", MastodonAPIController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks)
@ -331,16 +339,16 @@ defmodule Pleroma.Web.Router do
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
get("/domain_blocks", DomainBlockController, :index)
get("/filters", MastodonAPIController, :get_filters)
get("/filters", FilterController, :index)
get("/suggestions", MastodonAPIController, :suggestions)
@ -355,22 +363,22 @@ defmodule Pleroma.Web.Router do
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post("/statuses/:id/pin", MastodonAPIController, :pin_status)
post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
post("/polls/:id/votes", MastodonAPIController, :poll_vote)
@ -384,10 +392,10 @@ defmodule Pleroma.Web.Router do
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter)
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
@ -411,11 +419,11 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/mute", MastodonAPIController, :mute)
post("/accounts/:id/unmute", MastodonAPIController, :unmute)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
@ -448,10 +456,10 @@ defmodule Pleroma.Web.Router do
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/trends", MastodonAPIController, :empty_array)
@ -466,13 +474,13 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:oauth_read_or_public)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/timelines/list/:list_id", TimelineController, :list)
get("/statuses", MastodonAPIController, :get_statuses)
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", MastodonAPIController, :get_poll)

View file

@ -16,7 +16,7 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
"show.json",
activity: activity,
for: user
)
@ -43,7 +43,7 @@ def render("update.json", %Activity{} = activity) do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
"show.json",
activity: activity
)
|> Jason.encode!()

View file

@ -159,6 +159,9 @@ defp deps do
{:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.11.1", only: :test},
{:flake_id, "~> 0.1.0"},
{:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
{:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
end

View file

@ -48,6 +48,7 @@
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
"jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
@ -87,6 +88,7 @@
"quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.CreateSubscriptionNotifications do
use Ecto.Migration
def change do
create_if_not_exists table(:subscription_notifications) do
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all))
timestamps()
end
create_if_not_exists(index(:subscription_notifications, [:user_id]))
create_if_not_exists(index(:subscription_notifications, ["id desc nulls last"]))
end
end

View file

@ -68,7 +68,7 @@ test "receives well formatted events" do
assert {:ok, json} = Jason.decode(json["payload"])
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: activity, for: nil)
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|> Jason.encode!()
|> Jason.decode!()

View file

@ -33,16 +33,16 @@ test "notifies someone when they are directly addressed" do
assert other_notification.activity_id == activity.id
end
test "it creates a notification for subscribed users" do
test "it does not create a notification for subscribed users" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
{:ok, notifications} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
assert notifications == []
end
test "does not create a notification for subscribed users if status is a reply" do
@ -182,14 +182,16 @@ test "it doesn't create a notification for follow-unfollow-follow chains" do
refute Notification.create_notification(activity_dupe, followed_user)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
test "it doesn't create notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
{:ok, notifications} = Notification.create_notifications(status)
assert notifications == []
end
test "it doesn't create subscription notifications if the recipient cannot see the status" do

View file

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RemoteIpTest do
use ExUnit.Case, async: true
use Plug.Test
alias Pleroma.Plugs.RemoteIp
test "disabled" do
Pleroma.Config.put(RemoteIp, enabled: false)
%{remote_ip: remote_ip} = conn(:get, "/")
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == remote_ip
end
test "enabled" do
Pleroma.Config.put(RemoteIp, enabled: true)
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom headers" do
Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "1.1.1.1")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
conn =
conn(:get, "/")
|> put_req_header("cf-connecting-ip", "1.1.1.1")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
test "custom proxies" do
Pleroma.Config.put(RemoteIp, enabled: true)
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
refute conn.remote_ip == {1, 1, 1, 1}
Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"])
conn =
conn(:get, "/")
|> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
|> RemoteIp.call(nil)
assert conn.remote_ip == {1, 1, 1, 1}
end
end

View file

@ -61,7 +61,7 @@ test "includes reported statuses" do
AccountView.render("account.json", %{user: other_user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
statuses: [StatusView.render("status.json", %{activity: activity})],
statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
id: report_activity.id
}

View file

@ -0,0 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.User
import Pleroma.Factory
test "blocking / unblocking a domain", %{conn: conn} do
user = insert(:user)
other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)
conn =
build_conn()
|> assign(:user, user)
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user)
end
test "getting a list of domain blocks", %{conn: conn} do
user = insert(:user)
{:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")
domain_blocks = json_response(conn, 200)
assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
end
end

View file

@ -0,0 +1,137 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.MastodonAPI.FilterView
import Pleroma.Factory
test "creating a filter", %{conn: conn} do
user = insert(:user)
filter = %Pleroma.Filter{
phrase: "knights",
context: ["home"]
}
conn =
conn
|> assign(:user, user)
|> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
assert response = json_response(conn, 200)
assert response["phrase"] == filter.phrase
assert response["context"] == filter.context
assert response["irreversible"] == false
assert response["id"] != nil
assert response["id"] != ""
end
test "fetching a list of filters", %{conn: conn} do
user = insert(:user)
query_one = %Pleroma.Filter{
user_id: user.id,
filter_id: 1,
phrase: "knights",
context: ["home"]
}
query_two = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "who",
context: ["home"]
}
{:ok, filter_one} = Pleroma.Filter.create(query_one)
{:ok, filter_two} = Pleroma.Filter.create(query_two)
response =
conn
|> assign(:user, user)
|> get("/api/v1/filters")
|> json_response(200)
assert response ==
render_json(
FilterView,
"filters.json",
filters: [filter_two, filter_one]
)
end
test "get a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, filter} = Pleroma.Filter.create(query)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/filters/#{filter.filter_id}")
assert _response = json_response(conn, 200)
end
test "update a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, _filter} = Pleroma.Filter.create(query)
new = %Pleroma.Filter{
phrase: "nii",
context: ["home"]
}
conn =
conn
|> assign(:user, user)
|> put("/api/v1/filters/#{query.filter_id}", %{
phrase: new.phrase,
context: new.context
})
assert response = json_response(conn, 200)
assert response["phrase"] == new.phrase
assert response["context"] == new.context
end
test "delete a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, filter} = Pleroma.Filter.create(query)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/filters/#{filter.filter_id}")
assert response = json_response(conn, 200)
assert response == %{}
end
end

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
describe "locked accounts" do
test "/api/v1/follow_requests works" do
user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/follow_requests")
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
test "/api/v1/follow_requests/:id/authorize works" do
user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/authorize")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == true
end
test "/api/v1/follow_requests/:id/reject works" do
user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/reject")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
end
end
end

View file

@ -0,0 +1,113 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
import Pleroma.Factory
test "shows scheduled activities", %{conn: conn} do
user = insert(:user)
scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string()
scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string()
conn =
conn
|> assign(:user, user)
# min_id
conn_res =
conn
|> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
# since_id
conn_res =
conn
|> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
# max_id
conn_res =
conn
|> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
result = json_response(conn_res, 200)
assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
end
test "shows a scheduled activity", %{conn: conn} do
user = insert(:user)
scheduled_activity = insert(:scheduled_activity, user: user)
res_conn =
conn
|> assign(:user, user)
|> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
assert scheduled_activity_id == scheduled_activity.id |> to_string()
res_conn =
conn
|> assign(:user, user)
|> get("/api/v1/scheduled_statuses/404")
assert %{"error" => "Record not found"} = json_response(res_conn, 404)
end
test "updates a scheduled activity", %{conn: conn} do
user = insert(:user)
scheduled_activity = insert(:scheduled_activity, user: user)
new_scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
res_conn =
conn
|> assign(:user, user)
|> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
scheduled_at: new_scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
res_conn =
conn
|> assign(:user, user)
|> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
assert %{"error" => "Record not found"} = json_response(res_conn, 404)
end
test "deletes a scheduled activity", %{conn: conn} do
user = insert(:user)
scheduled_activity = insert(:scheduled_activity, user: user)
res_conn =
conn
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{} = json_response(res_conn, 200)
assert nil == Repo.get(ScheduledActivity, scheduled_activity.id)
res_conn =
conn
|> assign(:user, user)
|> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
assert %{"error" => "Record not found"} = json_response(res_conn, 404)
end
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,291 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus
clear_config([:instance, :public])
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "the home timeline", %{conn: conn} do
user = insert(:user)
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert Enum.empty?(json_response(conn, :ok))
{:ok, user} = User.follow(user, following)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, :ok)
end
describe "public" do
@tag capture_log: true
test "the public timeline", %{conn: conn} do
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
{:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, :ok)) == 2
conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, :ok)
conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, :ok)
end
test "the public timeline when public is set to false", %{conn: conn} do
Config.put([:instance, :public], false)
assert %{"error" => "This resource requires authentication."} ==
conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
|> json_response(:forbidden)
end
test "the public timeline includes only public statuses for an authenticated user" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response(res_conn, 200)) == 1
end
end
describe "direct" do
test "direct timeline", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
# Only direct should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"]
# User should be able to see their own direct message
res_conn =
build_conn()
|> assign(:user, user_one)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert %{"visibility" => "direct"} = status
# Both should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/home")
[_s1, _s2] = json_response(res_conn, :ok)
# Test pagination
Enum.each(1..20, fn _ ->
{:ok, _} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
end)
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
statuses = json_response(res_conn, :ok)
assert length(statuses) == 20
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
[status] = json_response(res_conn, :ok)
assert status["url"] != direct.data["id"]
end
test "doesn't include DMs from blocked users", %{conn: conn} do
blocker = insert(:user)
blocked = insert(:user)
user = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _blocked_direct} =
CommonAPI.post(blocked, %{
"status" => "Hi @#{blocker.nickname}!",
"visibility" => "direct"
})
{:ok, direct} =
CommonAPI.post(user, %{
"status" => "Hi @#{blocker.nickname}!",
"visibility" => "direct"
})
res_conn =
conn
|> assign(:user, user)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert status["id"] == direct.id
end
end
describe "list" do
test "list timeline", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
assert id == to_string(activity_two.id)
end
test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, _activity_two} =
CommonAPI.post(other_user, %{
"status" => "Marisa is cute.",
"visibility" => "private"
})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
assert id == to_string(activity_one.id)
end
end
describe "hashtag" do
@tag capture_log: true
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
{:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
{:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
nconn = get(conn, "/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(nconn, :ok)
assert id == to_string(activity.id)
# works for different capitalization too
nconn = get(conn, "/api/v1/timelines/tag/2HU")
assert [%{"id" => id}] = json_response(nconn, :ok)
assert id == to_string(activity.id)
end
test "multi-hashtag timeline", %{conn: conn} do
user = insert(:user)
{:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
{:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
{:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
any_test = get(conn, "/api/v1/timelines/tag/test", %{"any" => ["test1"]})
[status_none, status_test1, status_test] = json_response(any_test, :ok)
assert to_string(activity_test.id) == status_test["id"]
assert to_string(activity_test1.id) == status_test1["id"]
assert to_string(activity_none.id) == status_none["id"]
restricted_test =
get(conn, "/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
assert [status_test1] == json_response(restricted_test, :ok)
all_test = get(conn, "/api/v1/timelines/tag/test", %{"all" => ["none"]})
assert [status_none] == json_response(all_test, :ok)
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -75,9 +75,9 @@ test "returns notifications for user" do
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin @#{subscriber.nickname}"})
{:ok, status1} = CommonAPI.post(user, %{"status" => "Magi"})
{:ok, status1} = CommonAPI.post(user, %{"status" => "Magi @#{subscriber.nickname}"})
{:ok, [notification]} = Notification.create_notifications(status)
{:ok, [notification1]} = Notification.create_notifications(status1)
res = MastodonAPI.get_notifications(subscriber)

View file

@ -28,7 +28,7 @@ test "Mention notification" do
pleroma: %{is_seen: false},
type: "mention",
account: AccountView.render("account.json", %{user: user, for: mentioned_user}),
status: StatusView.render("status.json", %{activity: activity, for: mentioned_user}),
status: StatusView.render("show.json", %{activity: activity, for: mentioned_user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@ -51,7 +51,7 @@ test "Favourite notification" do
pleroma: %{is_seen: false},
type: "favourite",
account: AccountView.render("account.json", %{user: another_user, for: user}),
status: StatusView.render("status.json", %{activity: create_activity, for: user}),
status: StatusView.render("show.json", %{activity: create_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}
@ -73,7 +73,7 @@ test "Reblog notification" do
pleroma: %{is_seen: false},
type: "reblog",
account: AccountView.render("account.json", %{user: another_user, for: user}),
status: StatusView.render("status.json", %{activity: reblog_activity, for: user}),
status: StatusView.render("show.json", %{activity: reblog_activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at)
}

View file

@ -29,7 +29,7 @@ test "returns the direct conversation id when given the `with_conversation_id` o
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
status =
StatusView.render("status.json",
StatusView.render("show.json",
activity: activity,
with_direct_conversation_id: true,
for: user
@ -46,7 +46,7 @@ test "returns a temporary ap_id based user for activities missing db users" do
Repo.delete(user)
Cachex.clear(:user_cache)
%{account: ms_user} = StatusView.render("status.json", activity: activity)
%{account: ms_user} = StatusView.render("show.json", activity: activity)
assert ms_user.acct == "erroruser@example.com"
end
@ -63,7 +63,7 @@ test "tries to get a user by nickname if fetching by ap_id doesn't work" do
Cachex.clear(:user_cache)
result = StatusView.render("status.json", activity: activity)
result = StatusView.render("show.json", activity: activity)
assert result[:account][:id] == to_string(user.id)
end
@ -81,7 +81,7 @@ test "a note with null content" do
User.get_cached_by_ap_id(note.data["actor"])
status = StatusView.render("status.json", %{activity: note})
status = StatusView.render("show.json", %{activity: note})
assert status.content == ""
end
@ -93,7 +93,7 @@ test "a note activity" do
convo_id = Utils.context_to_conversation_id(object_data["context"])
status = StatusView.render("status.json", %{activity: note})
status = StatusView.render("show.json", %{activity: note})
created_at =
(object_data["published"] || "")
@ -165,11 +165,11 @@ test "tells if the message is muted for some reason" do
{:ok, user} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("status.json", %{activity: activity})
status = StatusView.render("show.json", %{activity: activity})
assert status.muted == false
status = StatusView.render("status.json", %{activity: activity, for: user})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.muted == true
end
@ -181,13 +181,13 @@ test "tells if the message is thread muted" do
{:ok, user} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("status.json", %{activity: activity, for: user})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == false
{:ok, activity} = CommonAPI.add_mute(user, activity)
status = StatusView.render("status.json", %{activity: activity, for: user})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == true
end
@ -196,11 +196,11 @@ test "tells if the status is bookmarked" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
status = StatusView.render("status.json", %{activity: activity})
status = StatusView.render("show.json", %{activity: activity})
assert status.bookmarked == false
status = StatusView.render("status.json", %{activity: activity, for: user})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == false
@ -208,7 +208,7 @@ test "tells if the status is bookmarked" do
activity = Activity.get_by_id_with_object(activity.id)
status = StatusView.render("status.json", %{activity: activity, for: user})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == true
end
@ -220,7 +220,7 @@ test "a reply" do
{:ok, activity} =
CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
status = StatusView.render("status.json", %{activity: activity})
status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == to_string(note.id)
@ -237,7 +237,7 @@ test "contains mentions" do
{:ok, [activity]} = OStatus.handle_incoming(incoming)
status = StatusView.render("status.json", %{activity: activity})
status = StatusView.render("show.json", %{activity: activity})
assert status.mentions ==
Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end)
@ -263,7 +263,7 @@ test "create mentions from the 'to' field" do
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient_ap_id
@ -300,7 +300,7 @@ test "create mentions from the 'tag' field" do
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient.ap_id
@ -340,7 +340,7 @@ test "put the url advertised in the Activity in to the url attribute" do
id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
[activity] = Activity.search(nil, id)
status = StatusView.render("status.json", %{activity: activity})
status = StatusView.render("show.json", %{activity: activity})
assert status.uri == id
assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
@ -352,7 +352,7 @@ test "a reblog" do
{:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
represented = StatusView.render("status.json", %{for: user, activity: reblog})
represented = StatusView.render("show.json", %{for: user, activity: reblog})
assert represented[:id] == to_string(reblog.id)
assert represented[:reblog][:id] == to_string(activity.id)
@ -369,7 +369,7 @@ test "a peertube video" do
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("status.json", %{for: user, activity: activity})
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
@ -570,7 +570,7 @@ test "embeds a relationship in the account" do
"status" => "drink more water"
})
result = StatusView.render("status.json", %{activity: activity, for: other_user})
result = StatusView.render("show.json", %{activity: activity, for: other_user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: other_user, target: user})
@ -587,7 +587,7 @@ test "embeds a relationship in the account in reposts" do
{:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
result = StatusView.render("status.json", %{activity: activity, for: user})
result = StatusView.render("show.json", %{activity: activity, for: user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: other_user})
@ -604,7 +604,7 @@ test "visibility/list" do
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
status = StatusView.render("status.json", activity: activity)
status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end

View file

@ -0,0 +1,234 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Repo
alias Pleroma.SubscriptionNotification
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance, :public])
clear_config([:rich_media, :enabled])
describe "subscription_notifications" do
setup do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, %{user: user, subscriber: subscriber}}
end
test "list of notifications", %{conn: conn, user: user, subscriber: subscriber} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
path = subscription_notification_path(conn, :index)
conn =
conn
|> assign(:user, subscriber)
|> get(path)
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == status_text
end
test "getting a single notification", %{conn: conn, user: user, subscriber: subscriber} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
[notification] = Repo.all(SubscriptionNotification)
path = subscription_notification_path(conn, :show, notification)
conn =
conn
|> assign(:user, subscriber)
|> get(path)
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == status_text
end
test "dismissing a single notification also deletes it", %{
conn: conn,
user: user,
subscriber: subscriber
} do
status_text = "Hello"
{:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
[notification] = Repo.all(SubscriptionNotification)
conn =
conn
|> assign(:user, subscriber)
|> post(subscription_notification_path(conn, :dismiss), %{"id" => notification.id})
assert %{} = json_response(conn, 200)
assert Repo.all(SubscriptionNotification) == []
end
test "clearing all notifications also deletes them", %{
conn: conn,
user: user,
subscriber: subscriber
} do
status_text1 = "Hello"
status_text2 = "Hello again"
{:ok, _activity1} = CommonAPI.post(user, %{"status" => status_text1})
{:ok, _activity2} = CommonAPI.post(user, %{"status" => status_text2})
conn =
conn
|> assign(:user, subscriber)
|> post(subscription_notification_path(conn, :clear))
assert %{} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, subscriber)
|> get(subscription_notification_path(conn, :index))
assert json_response(conn, 200) == []
assert Repo.all(SubscriptionNotification) == []
end
test "paginates notifications using min_id, since_id, max_id, and limit", %{
conn: conn,
user: user,
subscriber: subscriber
} do
{:ok, activity1} = CommonAPI.post(user, %{"status" => "Hello 1"})
{:ok, activity2} = CommonAPI.post(user, %{"status" => "Hello 2"})
{:ok, activity3} = CommonAPI.post(user, %{"status" => "Hello 3"})
{:ok, activity4} = CommonAPI.post(user, %{"status" => "Hello 4"})
notification1_id =
Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
notification2_id =
Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
notification3_id =
Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
notification4_id =
Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
conn = assign(conn, :user, subscriber)
# min_id
conn_res =
get(
conn,
subscription_notification_path(conn, :index, %{
"limit" => 2,
"min_id" => notification1_id
})
)
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
# since_id
conn_res =
get(
conn,
subscription_notification_path(conn, :index, %{
"limit" => 2,
"since_id" => notification1_id
})
)
result = json_response(conn_res, 200)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
# max_id
conn_res =
get(
conn,
subscription_notification_path(conn, :index, %{
"limit" => 2,
"max_id" => notification4_id
})
)
result = json_response(conn_res, 200)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
test "destroy multiple", %{conn: conn, user: user1, subscriber: user2} do
# mutual subscription
User.subscribe(user1, user2)
{:ok, activity1} = CommonAPI.post(user1, %{"status" => "Hello 1"})
{:ok, activity2} = CommonAPI.post(user1, %{"status" => "World 1"})
{:ok, activity3} = CommonAPI.post(user2, %{"status" => "Hello 2"})
{:ok, activity4} = CommonAPI.post(user2, %{"status" => "World 2"})
notification1_id =
Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
notification2_id =
Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
notification3_id =
Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
notification4_id =
Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
conn = assign(conn, :user, user1)
conn_res = get(conn, subscription_notification_path(conn, :index))
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification3_id, notification4_id]
end)
conn2 = assign(conn, :user, user2)
conn_res = get(conn2, subscription_notification_path(conn, :index))
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification1_id, notification2_id]
end)
conn_destroy =
delete(conn, subscription_notification_path(conn, :destroy_multiple), %{
"ids" => [notification3_id, notification4_id]
})
assert json_response(conn_destroy, 200) == %{}
conn_res = get(conn2, subscription_notification_path(conn, :index))
result = json_response(conn_res, 200)
Enum.each(result, fn %{"id" => id} ->
assert id in [notification1_id, notification2_id]
end)
assert length(Repo.all(SubscriptionNotification)) == 2
end
end
end