diff options
56 files changed, 3984 insertions, 2792 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d9ddb5b03..1d307f0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config/config.exs b/config/config.exs index 403ade60d..36bea19a0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -591,6 +591,8 @@ config :pleroma, :rate_limit, nil  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 diff --git a/config/description.exs b/config/description.exs index 38b30bbf6..4547ea368 100644 --- a/config/description.exs +++ b/config/description.exs @@ -2689,6 +2689,42 @@ config :pleroma, :config_description, [    },    %{      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,      type: :group,      description: diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 573111416..fcdb33944 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -711,6 +711,7 @@ Compile time settings (need instance reboot):      }    ]  } +```  - Response: diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index a469ddfbf..ac5489aa3 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -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` diff --git a/docs/config.md b/docs/config.md index ed119fd32..262d15bba 100644 --- a/docs/config.md +++ b/docs/config.md @@ -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). diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index 1f300f353..f200362ca 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -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 don’t have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l pleroma -s $SHELL -c 'command'` and `su -l pleroma -s $SHELL` for starting a shell. @@ -164,7 +168,26 @@ If that doesn’t 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 diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index caf72363b..5ca6b3634 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -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 ユーザーでインストール。 -- erlang-dev +- 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  ``` -#### Systemd サービス +もし証明書を更新する必要が出てきた場合には、nginxの関連するlocationブロックのコメントアウトを外し、以下のコマンドを動かします。 -* サービスファイルの例をコピーします。  ``` -cp /home/pleroma/pleroma/installation/pleroma.service /usr/lib/systemd/system/pleroma.service +sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/  ``` -* サービスファイルを変更します。すべてのパスが正しいことを確認してください。また、`[Service]` セクションに以下の行があることを確認してください。 -``` -Environment="MIX_ENV=prod" -``` +#### 他のWebサーバやプロキシ +これに関してはサンプルが `/opt/pleroma/installation/` にあるので、探してみてください。 + +#### Systemd サービス -* `pleroma.service` を enable および start してください。 +* サービスファイルのサンプルをコピーします。  ``` -systemctl enable --now pleroma.service +sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service  ``` -#### モデレーターを作る - -新たにユーザーを作ったら、モデレーター権限を与えたいかもしれません。以下のタスクで可能です。 +* サービスファイルを変更します。すべてのパスが正しいことを確認してください +* サービスを有効化し `pleroma.service` を開始してください  ``` -mix set_moderator username [true|false] +sudo systemctl enable --now pleroma.service  ``` -モデレーターはすべてのポストを消すことができます。将来的には他のことも可能になるかもしれません。 +#### 初期ユーザの作成 -#### メディアプロクシを有効にする +新たにインスタンスを作成したら、以下のコマンドにより管理者権限を持った初期ユーザを作成できます。 -`generate_config` でメディアプロクシを有効にしているなら、すでにメディアプロクシが動作しています。あとから設定を変更したいなら、[How to activate mediaproxy](How-to-activate-mediaproxy) を見てください。 +``` +sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin +``` -#### コンフィギュレーションとカスタマイズ +#### その他の設定とカスタマイズ  * [Backup your instance](backup.html)  * [Configuration tips](general-tips-for-customizing-pleroma-fe.html) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 4da9918ca..7f48b614b 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -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 diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 0a381f592..fa838a4e4 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -42,7 +42,7 @@ defmodule Pleroma.BBS.Handler 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("") diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d94ae5971..d19924289 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -230,7 +230,6 @@ defmodule Pleroma.Notification do        []        |> 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) diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex new file mode 100644 index 000000000..fdedc27ee --- /dev/null +++ b/lib/pleroma/plugs/remote_ip.ex @@ -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 diff --git a/lib/pleroma/subscription_notification.ex b/lib/pleroma/subscription_notification.ex new file mode 100644 index 000000000..1349d988c --- /dev/null +++ b/lib/pleroma/subscription_notification.ex @@ -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 diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8d0a57623..7e83e27e5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})        Notification.create_notifications(activity) +      SubscriptionNotification.create_notifications(activity)        participations =          activity diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 90aef99f7..21da8a7ff 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -513,7 +513,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        conn        |> put_view(StatusView) -      |> render("status.json", %{activity: activity}) +      |> render("show.json", %{activity: activity})      else        true ->          {:param_cast, nil} @@ -537,7 +537,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do        conn        |> put_view(StatusView) -      |> render("status.json", %{activity: activity}) +      |> render("show.json", %{activity: activity})      end    end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex new file mode 100644 index 000000000..f7da81b34 --- /dev/null +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -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 diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 4a74dc16f..a00e4b0d8 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI 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,33 +136,41 @@ defmodule Pleroma.Web.CommonAPI do        object = Object.get_cached_by_ap_id(object.data["id"])        {:ok, answer_activities, object} -    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")}      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 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 -      {object.data["oneOf"], 1} +      {:error, dgettext("errors", "Already voted")}      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 +  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) -  def get_visibility(_, _, %Participation{}) do -    {"direct", "direct"} +    with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))}, +         {_, true} <- {:count_check, Enum.count(choices) <= max_count} do +      {:ok, options, choices} +    else +      {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")} +      {:count_check, _} -> {:error, dgettext("errors", "Too many choices")} +    end    end +  def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} +    def get_visibility(%{"visibility" => visibility}, in_reply_to, _)        when visibility in ~w{public unlisted private direct},        do: {visibility, get_replied_to_visibility(in_reply_to)} @@ -197,13 +191,13 @@ defmodule Pleroma.Web.CommonAPI do    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 @@ defmodule Pleroma.Web.CommonAPI 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) -        end -      end - -      result -    else -      {:private_to_public, true} -> -        {:error, dgettext("errors", "The message visibility must be direct")} - -      {:error, _} = e -> -        e - -      e -> -        {:error, e} +  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 -  # 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) -    ) +  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do +    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do +      {:ok, activity} +    end    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 @@ defmodule Pleroma.Web.CommonAPI 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,51 +289,46 @@ defmodule Pleroma.Web.CommonAPI 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} <- -           ActivityPub.flag(%{ -             context: Utils.generate_context_id(), -             actor: user, -             account: account, -             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")} +         {:ok, statuses} <- get_report_statuses(account, data) do +      ActivityPub.flag(%{ +        context: Utils.generate_context_id(), +        actor: user, +        account: account, +        statuses: statuses, +        content: content_html, +        forward: data["forward"] || false +      }) +    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} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 52fbc162b..88a5f434a 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 +    limits = Pleroma.Config.get([:instance, :poll_limits]) -      {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 - -          {%{ -             "name" => option, -             "type" => "Note", -             "replies" => %{"type" => "Collection", "totalItems" => 0} -           }, 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" +          note = %{ +            "name" => option, +            "type" => "Note", +            "replies" => %{"type" => "Collection", "totalItems" => 0} +          } -        _ -> -          :noop -      end +          {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))} +        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 @@ defmodule Pleroma.Web.CommonAPI.Utils do      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 @@ defmodule Pleroma.Web.CommonAPI.Utils do          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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 +      :ok      else        {:error, dgettext("errors", "The status is over the character limit")}      end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b53a01955..e90bf842e 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -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 diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index eb805e853..2212e93f4 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex new file mode 100644 index 000000000..03db6c9b8 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex new file mode 100644 index 000000000..19041304e --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex new file mode 100644 index 000000000..267014b97 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 8f6b3456a..0878f7ba6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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,15 +1054,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  defp try_render(conn, target, params) -       when is_binary(target) do +  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")        res -> res      end    end -  defp try_render(conn, _, _) do +  def try_render(conn, _, _) do      render_error(conn, :not_implemented, "Can't display this activity")    end diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex new file mode 100644 index 000000000..0a56b10b6 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex new file mode 100644 index 000000000..f4de9285b --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex new file mode 100644 index 000000000..bb8b0eb32 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 40acc07b3..4aeb79d81 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -24,7 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView 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 = diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index ec8eadcaa..05110a192 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -39,19 +39,19 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do        "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" -> diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex index 0aae15ab9..fc042a276 100644 --- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -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 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do    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 +    attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment) +    Map.put(data, :media_attachments, attachments)    end    defp with_media_attachments(data, _), do: data @@ -45,13 +40,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView 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 diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0450ed4d9..2321d0de2 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -73,17 +73,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        |> 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      }    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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      }    end -  def render("status.json", _) do +  def render("show.json", _) do      nil    end @@ -441,6 +437,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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) diff --git a/lib/pleroma/web/pleroma_api/controllers/subscription_notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/subscription_notification_controller.ex new file mode 100644 index 000000000..37c2222de --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/subscription_notification_controller.ex @@ -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 diff --git a/lib/pleroma/web/pleroma_api/pleroma_api.ex b/lib/pleroma/web/pleroma_api/pleroma_api.ex new file mode 100644 index 000000000..480964845 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/pleroma_api.ex @@ -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 diff --git a/lib/pleroma/web/pleroma_api/views/subscription_notification_view.ex b/lib/pleroma/web/pleroma_api/views/subscription_notification_view.ex new file mode 100644 index 000000000..fc41a7389 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/subscription_notification_view.ex @@ -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 diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 35d3ff07c..7ea5607fa 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -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, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 316c895ee..a025474e2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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) diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index b13030fa0..a9f14d09a 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.StreamerView do        event: "update",        payload:          Pleroma.Web.MastodonAPI.StatusView.render( -          "status.json", +          "show.json",            activity: activity,            for: user          ) @@ -43,7 +43,7 @@ defmodule Pleroma.Web.StreamerView do        event: "update",        payload:          Pleroma.Web.MastodonAPI.StatusView.render( -          "status.json", +          "show.json",            activity: activity          )          |> Jason.encode!() @@ -159,6 +159,9 @@ defmodule Pleroma.Mixfile 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 @@ -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"}, diff --git a/priv/repo/migrations/20190824195028_create_subscription_notifications.exs b/priv/repo/migrations/20190824195028_create_subscription_notifications.exs new file mode 100644 index 000000000..fcceb4386 --- /dev/null +++ b/priv/repo/migrations/20190824195028_create_subscription_notifications.exs @@ -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 diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs index ed7ce8fe0..63fce07bb 100644 --- a/test/integration/mastodon_websocket_test.exs +++ b/test/integration/mastodon_websocket_test.exs @@ -68,7 +68,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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!() diff --git a/test/notification_test.exs b/test/notification_test.exs index 54c0f9877..1dbad34c1 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -33,16 +33,16 @@ defmodule Pleroma.NotificationTest 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 @@ defmodule Pleroma.NotificationTest 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 diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs new file mode 100644 index 000000000..d120c588b --- /dev/null +++ b/test/plugs/remote_ip_test.exs @@ -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 diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs index 40df01101..35b6947a0 100644 --- a/test/web/admin_api/views/report_view_test.exs +++ b/test/web/admin_api/views/report_view_test.exs @@ -61,7 +61,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest 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      } diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs new file mode 100644 index 000000000..3c3558385 --- /dev/null +++ b/test/web/mastodon_api/controllers/domain_block_controller_test.exs @@ -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 diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs new file mode 100644 index 000000000..5d5b56c8e --- /dev/null +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -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 diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs new file mode 100644 index 000000000..4bf292df5 --- /dev/null +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -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 diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs new file mode 100644 index 000000000..9ad6a4fa7 --- /dev/null +++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -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 diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..b194feae6 --- /dev/null +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -0,0 +1,1210 @@ +# 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.StatusControllerTest do +  use Pleroma.Web.ConnCase + +  alias Pleroma.Activity +  alias Pleroma.ActivityExpiration +  alias Pleroma.Config +  alias Pleroma.Object +  alias Pleroma.Repo +  alias Pleroma.ScheduledActivity +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.CommonAPI + +  import Pleroma.Factory + +  describe "posting statuses" do +    setup do +      user = insert(:user) + +      conn = +        build_conn() +        |> assign(:user, user) + +      [conn: conn] +    end + +    test "posting a status", %{conn: conn} do +      idempotency_key = "Pikachu rocks!" + +      conn_one = +        conn +        |> put_req_header("idempotency-key", idempotency_key) +        |> post("/api/v1/statuses", %{ +          "status" => "cofe", +          "spoiler_text" => "2hu", +          "sensitive" => "false" +        }) + +      {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) +      # Six hours +      assert ttl > :timer.seconds(6 * 60 * 60 - 1) + +      assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = +               json_response(conn_one, 200) + +      assert Activity.get_by_id(id) + +      conn_two = +        conn +        |> put_req_header("idempotency-key", idempotency_key) +        |> post("/api/v1/statuses", %{ +          "status" => "cofe", +          "spoiler_text" => "2hu", +          "sensitive" => "false" +        }) + +      assert %{"id" => second_id} = json_response(conn_two, 200) +      assert id == second_id + +      conn_three = +        conn +        |> post("/api/v1/statuses", %{ +          "status" => "cofe", +          "spoiler_text" => "2hu", +          "sensitive" => "false" +        }) + +      assert %{"id" => third_id} = json_response(conn_three, 200) +      refute id == third_id + +      # An activity that will expire: +      # 2 hours +      expires_in = 120 * 60 + +      conn_four = +        conn +        |> post("api/v1/statuses", %{ +          "status" => "oolong", +          "expires_in" => expires_in +        }) + +      assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200) +      assert activity = Activity.get_by_id(fourth_id) +      assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) + +      estimated_expires_at = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(expires_in) +        |> NaiveDateTime.truncate(:second) + +      # This assert will fail if the test takes longer than a minute. I sure hope it never does: +      assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 + +      assert fourth_response["pleroma"]["expires_at"] == +               NaiveDateTime.to_iso8601(expiration.scheduled_at) +    end + +    test "posting an undefined status with an attachment", %{conn: conn} do +      user = insert(:user) + +      file = %Plug.Upload{ +        content_type: "image/jpg", +        path: Path.absname("test/fixtures/image.jpg"), +        filename: "an_image.jpg" +      } + +      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "media_ids" => [to_string(upload.id)] +        }) + +      assert json_response(conn, 200) +    end + +    test "replying to a status", %{conn: conn} do +      user = insert(:user) +      {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"}) + +      conn = +        conn +        |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + +      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) + +      activity = Activity.get_by_id(id) + +      assert activity.data["context"] == replied_to.data["context"] +      assert Activity.get_in_reply_to_activity(activity).id == replied_to.id +    end + +    test "replying to a direct message with visibility other than direct", %{conn: conn} do +      user = insert(:user) +      {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"}) + +      Enum.each(["public", "private", "unlisted"], fn visibility -> +        conn = +          conn +          |> post("/api/v1/statuses", %{ +            "status" => "@#{user.nickname} hey", +            "in_reply_to_id" => replied_to.id, +            "visibility" => visibility +          }) + +        assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"} +      end) +    end + +    test "posting a status with an invalid in_reply_to_id", %{conn: conn} do +      conn = +        conn +        |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) + +      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) +      assert Activity.get_by_id(id) +    end + +    test "posting a sensitive status", %{conn: conn} do +      conn = +        conn +        |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) + +      assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) +      assert Activity.get_by_id(id) +    end + +    test "posting a fake status", %{conn: conn} do +      real_conn = +        conn +        |> post("/api/v1/statuses", %{ +          "status" => +            "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" +        }) + +      real_status = json_response(real_conn, 200) + +      assert real_status +      assert Object.get_by_ap_id(real_status["uri"]) + +      real_status = +        real_status +        |> Map.put("id", nil) +        |> Map.put("url", nil) +        |> Map.put("uri", nil) +        |> Map.put("created_at", nil) +        |> Kernel.put_in(["pleroma", "conversation_id"], nil) + +      fake_conn = +        conn +        |> post("/api/v1/statuses", %{ +          "status" => +            "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", +          "preview" => true +        }) + +      fake_status = json_response(fake_conn, 200) + +      assert fake_status +      refute Object.get_by_ap_id(fake_status["uri"]) + +      fake_status = +        fake_status +        |> Map.put("id", nil) +        |> Map.put("url", nil) +        |> Map.put("uri", nil) +        |> Map.put("created_at", nil) +        |> Kernel.put_in(["pleroma", "conversation_id"], nil) + +      assert real_status == fake_status +    end + +    test "posting a status with OGP link preview", %{conn: conn} do +      Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) +      Config.put([:rich_media, :enabled], true) + +      conn = +        conn +        |> post("/api/v1/statuses", %{ +          "status" => "https://example.com/ogp" +        }) + +      assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200) +      assert Activity.get_by_id(id) +    end + +    test "posting a direct status", %{conn: conn} do +      user2 = insert(:user) +      content = "direct cofe @#{user2.nickname}" + +      conn = +        conn +        |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + +      assert %{"id" => id} = response = json_response(conn, 200) +      assert response["visibility"] == "direct" +      assert response["pleroma"]["direct_conversation_id"] +      assert activity = Activity.get_by_id(id) +      assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] +      assert activity.data["to"] == [user2.ap_id] +      assert activity.data["cc"] == [] +    end +  end + +  describe "posting scheduled statuses" do +    test "creates a scheduled activity", %{conn: conn} do +      user = insert(:user) +      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200) +      assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at) +      assert [] == Repo.all(Activity) +    end + +    test "creates a scheduled activity with a media attachment", %{conn: conn} do +      user = insert(:user) +      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + +      file = %Plug.Upload{ +        content_type: "image/jpg", +        path: Path.absname("test/fixtures/image.jpg"), +        filename: "an_image.jpg" +      } + +      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "media_ids" => [to_string(upload.id)], +          "status" => "scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200) +      assert %{"type" => "image"} = media_attachment +    end + +    test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", +         %{conn: conn} do +      user = insert(:user) + +      scheduled_at = +        NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "not scheduled", +          "scheduled_at" => scheduled_at +        }) + +      assert %{"content" => "not scheduled"} = json_response(conn, 200) +      assert [] == Repo.all(ScheduledActivity) +    end + +    test "returns error when daily user limit is exceeded", %{conn: conn} do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: today} +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, attrs) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + +      assert %{"error" => "daily limit exceeded"} == json_response(conn, 422) +    end + +    test "returns error when total user limit is exceeded", %{conn: conn} do +      user = insert(:user) + +      today = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      tomorrow = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(:timer.hours(36), :millisecond) +        |> NaiveDateTime.to_iso8601() + +      attrs = %{params: %{}, scheduled_at: today} +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, attrs) +      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + +      assert %{"error" => "total limit exceeded"} == json_response(conn, 422) +    end +  end + +  describe "posting polls" do +    test "posting a poll", %{conn: conn} do +      user = insert(:user) +      time = NaiveDateTime.utc_now() + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "Who is the #bestgrill?", +          "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} +        }) + +      response = json_response(conn, 200) + +      assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> +               title in ["Rei", "Asuka", "Misato"] +             end) + +      assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 +      refute response["poll"]["expred"] +    end + +    test "option limit is enforced", %{conn: conn} do +      user = insert(:user) +      limit = Config.get([:instance, :poll_limits, :max_options]) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "desu~", +          "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} +        }) + +      %{"error" => error} = json_response(conn, 422) +      assert error == "Poll can't contain more than #{limit} options" +    end + +    test "option character limit is enforced", %{conn: conn} do +      user = insert(:user) +      limit = Config.get([:instance, :poll_limits, :max_option_chars]) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "...", +          "poll" => %{ +            "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], +            "expires_in" => 1 +          } +        }) + +      %{"error" => error} = json_response(conn, 422) +      assert error == "Poll options cannot be longer than #{limit} characters each" +    end + +    test "minimal date limit is enforced", %{conn: conn} do +      user = insert(:user) +      limit = Config.get([:instance, :poll_limits, :min_expiration]) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "imagine arbitrary limits", +          "poll" => %{ +            "options" => ["this post was made by pleroma gang"], +            "expires_in" => limit - 1 +          } +        }) + +      %{"error" => error} = json_response(conn, 422) +      assert error == "Expiration date is too soon" +    end + +    test "maximum date limit is enforced", %{conn: conn} do +      user = insert(:user) +      limit = Config.get([:instance, :poll_limits, :max_expiration]) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses", %{ +          "status" => "imagine arbitrary limits", +          "poll" => %{ +            "options" => ["this post was made by pleroma gang"], +            "expires_in" => limit + 1 +          } +        }) + +      %{"error" => error} = json_response(conn, 422) +      assert error == "Expiration date is too far in the future" +    end +  end + +  test "get a status", %{conn: conn} do +    activity = insert(:note_activity) + +    conn = +      conn +      |> get("/api/v1/statuses/#{activity.id}") + +    assert %{"id" => id} = json_response(conn, 200) +    assert id == to_string(activity.id) +  end + +  test "get statuses by IDs", %{conn: conn} do +    %{id: id1} = insert(:note_activity) +    %{id: id2} = insert(:note_activity) + +    query_string = "ids[]=#{id1}&ids[]=#{id2}" +    conn = get(conn, "/api/v1/statuses/?#{query_string}") + +    assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"]) +  end + +  describe "deleting a status" do +    test "when you created it", %{conn: conn} do +      activity = insert(:note_activity) +      author = User.get_cached_by_ap_id(activity.data["actor"]) + +      conn = +        conn +        |> assign(:user, author) +        |> delete("/api/v1/statuses/#{activity.id}") + +      assert %{} = json_response(conn, 200) + +      refute Activity.get_by_id(activity.id) +    end + +    test "when you didn't create it", %{conn: conn} do +      activity = insert(:note_activity) +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> delete("/api/v1/statuses/#{activity.id}") + +      assert %{"error" => _} = json_response(conn, 403) + +      assert Activity.get_by_id(activity.id) == activity +    end + +    test "when you're an admin or moderator", %{conn: conn} do +      activity1 = insert(:note_activity) +      activity2 = insert(:note_activity) +      admin = insert(:user, info: %{is_admin: true}) +      moderator = insert(:user, info: %{is_moderator: true}) + +      res_conn = +        conn +        |> assign(:user, admin) +        |> delete("/api/v1/statuses/#{activity1.id}") + +      assert %{} = json_response(res_conn, 200) + +      res_conn = +        conn +        |> assign(:user, moderator) +        |> delete("/api/v1/statuses/#{activity2.id}") + +      assert %{} = json_response(res_conn, 200) + +      refute Activity.get_by_id(activity1.id) +      refute Activity.get_by_id(activity2.id) +    end +  end + +  describe "reblogging" do +    test "reblogs and returns the reblogged status", %{conn: conn} do +      activity = insert(:note_activity) +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{activity.id}/reblog") + +      assert %{ +               "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, +               "reblogged" => true +             } = json_response(conn, 200) + +      assert to_string(activity.id) == id +    end + +    test "reblogged status for another user", %{conn: conn} do +      activity = insert(:note_activity) +      user1 = insert(:user) +      user2 = insert(:user) +      user3 = insert(:user) +      CommonAPI.favorite(activity.id, user2) +      {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) +      {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1) +      {:ok, _, _object} = CommonAPI.repeat(activity.id, user2) + +      conn_res = +        conn +        |> assign(:user, user3) +        |> get("/api/v1/statuses/#{reblog_activity1.id}") + +      assert %{ +               "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, +               "reblogged" => false, +               "favourited" => false, +               "bookmarked" => false +             } = json_response(conn_res, 200) + +      conn_res = +        conn +        |> assign(:user, user2) +        |> get("/api/v1/statuses/#{reblog_activity1.id}") + +      assert %{ +               "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, +               "reblogged" => true, +               "favourited" => true, +               "bookmarked" => true +             } = json_response(conn_res, 200) + +      assert to_string(activity.id) == id +    end + +    test "returns 400 error when activity is not exist", %{conn: conn} do +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/foo/reblog") + +      assert json_response(conn, 400) == %{"error" => "Could not repeat"} +    end +  end + +  describe "unreblogging" do +    test "unreblogs and returns the unreblogged status", %{conn: conn} do +      activity = insert(:note_activity) +      user = insert(:user) + +      {:ok, _, _} = CommonAPI.repeat(activity.id, user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{activity.id}/unreblog") + +      assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200) + +      assert to_string(activity.id) == id +    end + +    test "returns 400 error when activity is not exist", %{conn: conn} do +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/foo/unreblog") + +      assert json_response(conn, 400) == %{"error" => "Could not unrepeat"} +    end +  end + +  describe "favoriting" do +    test "favs a status and returns it", %{conn: conn} do +      activity = insert(:note_activity) +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{activity.id}/favourite") + +      assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = +               json_response(conn, 200) + +      assert to_string(activity.id) == id +    end + +    test "returns 400 error for a wrong id", %{conn: conn} do +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/1/favourite") + +      assert json_response(conn, 400) == %{"error" => "Could not favorite"} +    end +  end + +  describe "unfavoriting" do +    test "unfavorites a status and returns it", %{conn: conn} do +      activity = insert(:note_activity) +      user = insert(:user) + +      {:ok, _, _} = CommonAPI.favorite(activity.id, user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{activity.id}/unfavourite") + +      assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = +               json_response(conn, 200) + +      assert to_string(activity.id) == id +    end + +    test "returns 400 error for a wrong id", %{conn: conn} do +      user = insert(:user) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/1/unfavourite") + +      assert json_response(conn, 400) == %{"error" => "Could not unfavorite"} +    end +  end + +  describe "pinned statuses" do +    setup do +      user = insert(:user) +      {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) + +      [user: user, activity: activity] +    end + +    clear_config([:instance, :max_pinned_statuses]) do +      Config.put([:instance, :max_pinned_statuses], 1) +    end + +    test "pin status", %{conn: conn, user: user, activity: activity} do +      id_str = to_string(activity.id) + +      assert %{"id" => ^id_str, "pinned" => true} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{activity.id}/pin") +               |> json_response(200) + +      assert [%{"id" => ^id_str, "pinned" => true}] = +               conn +               |> assign(:user, user) +               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") +               |> json_response(200) +    end + +    test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do +      {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"}) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{dm.id}/pin") + +      assert json_response(conn, 400) == %{"error" => "Could not pin"} +    end + +    test "unpin status", %{conn: conn, user: user, activity: activity} do +      {:ok, _} = CommonAPI.pin(activity.id, user) + +      id_str = to_string(activity.id) +      user = refresh_record(user) + +      assert %{"id" => ^id_str, "pinned" => false} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{activity.id}/unpin") +               |> json_response(200) + +      assert [] = +               conn +               |> assign(:user, user) +               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") +               |> json_response(200) +    end + +    test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/1/unpin") + +      assert json_response(conn, 400) == %{"error" => "Could not unpin"} +    end + +    test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do +      {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) + +      id_str_one = to_string(activity_one.id) + +      assert %{"id" => ^id_str_one, "pinned" => true} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{id_str_one}/pin") +               |> json_response(200) + +      user = refresh_record(user) + +      assert %{"error" => "You have already pinned the maximum number of statuses"} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{activity_two.id}/pin") +               |> json_response(400) +    end +  end + +  describe "cards" do +    setup do +      Config.put([:rich_media, :enabled], true) + +      user = insert(:user) +      %{user: user} +    end + +    test "returns rich-media card", %{conn: conn, user: user} do +      Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + +      {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"}) + +      card_data = %{ +        "image" => "http://ia.media-imdb.com/images/rock.jpg", +        "provider_name" => "example.com", +        "provider_url" => "https://example.com", +        "title" => "The Rock", +        "type" => "link", +        "url" => "https://example.com/ogp", +        "description" => +          "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", +        "pleroma" => %{ +          "opengraph" => %{ +            "image" => "http://ia.media-imdb.com/images/rock.jpg", +            "title" => "The Rock", +            "type" => "video.movie", +            "url" => "https://example.com/ogp", +            "description" => +              "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." +          } +        } +      } + +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/card") +        |> json_response(200) + +      assert response == card_data + +      # works with private posts +      {:ok, activity} = +        CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"}) + +      response_two = +        conn +        |> assign(:user, user) +        |> get("/api/v1/statuses/#{activity.id}/card") +        |> json_response(200) + +      assert response_two == card_data +    end + +    test "replaces missing description with an empty string", %{conn: conn, user: user} do +      Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + +      {:ok, activity} = +        CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"}) + +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/card") +        |> json_response(:ok) + +      assert response == %{ +               "type" => "link", +               "title" => "Pleroma", +               "description" => "", +               "image" => nil, +               "provider_name" => "example.com", +               "provider_url" => "https://example.com", +               "url" => "https://example.com/ogp-missing-data", +               "pleroma" => %{ +                 "opengraph" => %{ +                   "title" => "Pleroma", +                   "type" => "website", +                   "url" => "https://example.com/ogp-missing-data" +                 } +               } +             } +    end +  end + +  test "bookmarks" do +    user = insert(:user) +    for_user = insert(:user) + +    {:ok, activity1} = +      CommonAPI.post(user, %{ +        "status" => "heweoo?" +      }) + +    {:ok, activity2} = +      CommonAPI.post(user, %{ +        "status" => "heweoo!" +      }) + +    response1 = +      build_conn() +      |> assign(:user, for_user) +      |> post("/api/v1/statuses/#{activity1.id}/bookmark") + +    assert json_response(response1, 200)["bookmarked"] == true + +    response2 = +      build_conn() +      |> assign(:user, for_user) +      |> post("/api/v1/statuses/#{activity2.id}/bookmark") + +    assert json_response(response2, 200)["bookmarked"] == true + +    bookmarks = +      build_conn() +      |> assign(:user, for_user) +      |> get("/api/v1/bookmarks") + +    assert [json_response(response2, 200), json_response(response1, 200)] == +             json_response(bookmarks, 200) + +    response1 = +      build_conn() +      |> assign(:user, for_user) +      |> post("/api/v1/statuses/#{activity1.id}/unbookmark") + +    assert json_response(response1, 200)["bookmarked"] == false + +    bookmarks = +      build_conn() +      |> assign(:user, for_user) +      |> get("/api/v1/bookmarks") + +    assert [json_response(response2, 200)] == json_response(bookmarks, 200) +  end + +  describe "conversation muting" do +    setup do +      post_user = insert(:user) +      user = insert(:user) + +      {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"}) + +      [user: user, activity: activity] +    end + +    test "mute conversation", %{conn: conn, user: user, activity: activity} do +      id_str = to_string(activity.id) + +      assert %{"id" => ^id_str, "muted" => true} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{activity.id}/mute") +               |> json_response(200) +    end + +    test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do +      {:ok, _} = CommonAPI.add_mute(user, activity) + +      conn = +        conn +        |> assign(:user, user) +        |> post("/api/v1/statuses/#{activity.id}/mute") + +      assert json_response(conn, 400) == %{"error" => "conversation is already muted"} +    end + +    test "unmute conversation", %{conn: conn, user: user, activity: activity} do +      {:ok, _} = CommonAPI.add_mute(user, activity) + +      id_str = to_string(activity.id) +      user = refresh_record(user) + +      assert %{"id" => ^id_str, "muted" => false} = +               conn +               |> assign(:user, user) +               |> post("/api/v1/statuses/#{activity.id}/unmute") +               |> json_response(200) +    end +  end + +  test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do +    user1 = insert(:user) +    user2 = insert(:user) +    user3 = insert(:user) + +    {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"}) + +    # Reply to status from another user +    conn1 = +      conn +      |> assign(:user, user2) +      |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + +    assert %{"content" => "xD", "id" => id} = json_response(conn1, 200) + +    activity = Activity.get_by_id_with_object(id) + +    assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] +    assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + +    # Reblog from the third user +    conn2 = +      conn +      |> assign(:user, user3) +      |> post("/api/v1/statuses/#{activity.id}/reblog") + +    assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = +             json_response(conn2, 200) + +    assert to_string(activity.id) == id + +    # Getting third user status +    conn3 = +      conn +      |> assign(:user, user3) +      |> get("api/v1/timelines/home") + +    [reblogged_activity] = json_response(conn3, 200) + +    assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id + +    replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) +    assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id +  end + +  describe "GET /api/v1/statuses/:id/favourited_by" do +    setup do +      user = insert(:user) +      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) + +      conn = +        build_conn() +        |> assign(:user, user) + +      [conn: conn, activity: activity, user: user] +    end + +    test "returns users who have favorited the status", %{conn: conn, activity: activity} do +      other_user = insert(:user) +      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/favourited_by") +        |> json_response(:ok) + +      [%{"id" => id}] = response + +      assert id == other_user.id +    end + +    test "returns empty array when status has not been favorited yet", %{ +      conn: conn, +      activity: activity +    } do +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/favourited_by") +        |> json_response(:ok) + +      assert Enum.empty?(response) +    end + +    test "does not return users who have favorited the status but are blocked", %{ +      conn: %{assigns: %{user: user}} = conn, +      activity: activity +    } do +      other_user = insert(:user) +      {:ok, user} = User.block(user, other_user) + +      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + +      response = +        conn +        |> assign(:user, user) +        |> get("/api/v1/statuses/#{activity.id}/favourited_by") +        |> json_response(:ok) + +      assert Enum.empty?(response) +    end + +    test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do +      other_user = insert(:user) +      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + +      response = +        conn +        |> assign(:user, nil) +        |> get("/api/v1/statuses/#{activity.id}/favourited_by") +        |> json_response(:ok) + +      [%{"id" => id}] = response +      assert id == other_user.id +    end + +    test "requires authentification for private posts", %{conn: conn, user: user} do +      other_user = insert(:user) + +      {:ok, activity} = +        CommonAPI.post(user, %{ +          "status" => "@#{other_user.nickname} wanna get some #cofe together?", +          "visibility" => "direct" +        }) + +      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + +      conn +      |> assign(:user, nil) +      |> get("/api/v1/statuses/#{activity.id}/favourited_by") +      |> json_response(404) + +      response = +        build_conn() +        |> assign(:user, other_user) +        |> get("/api/v1/statuses/#{activity.id}/favourited_by") +        |> json_response(200) + +      [%{"id" => id}] = response +      assert id == other_user.id +    end +  end + +  describe "GET /api/v1/statuses/:id/reblogged_by" do +    setup do +      user = insert(:user) +      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) + +      conn = +        build_conn() +        |> assign(:user, user) + +      [conn: conn, activity: activity, user: user] +    end + +    test "returns users who have reblogged the status", %{conn: conn, activity: activity} do +      other_user = insert(:user) +      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) + +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +        |> json_response(:ok) + +      [%{"id" => id}] = response + +      assert id == other_user.id +    end + +    test "returns empty array when status has not been reblogged yet", %{ +      conn: conn, +      activity: activity +    } do +      response = +        conn +        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +        |> json_response(:ok) + +      assert Enum.empty?(response) +    end + +    test "does not return users who have reblogged the status but are blocked", %{ +      conn: %{assigns: %{user: user}} = conn, +      activity: activity +    } do +      other_user = insert(:user) +      {:ok, user} = User.block(user, other_user) + +      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) + +      response = +        conn +        |> assign(:user, user) +        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +        |> json_response(:ok) + +      assert Enum.empty?(response) +    end + +    test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do +      other_user = insert(:user) +      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) + +      response = +        conn +        |> assign(:user, nil) +        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +        |> json_response(:ok) + +      [%{"id" => id}] = response +      assert id == other_user.id +    end + +    test "requires authentification for private posts", %{conn: conn, user: user} do +      other_user = insert(:user) + +      {:ok, activity} = +        CommonAPI.post(user, %{ +          "status" => "@#{other_user.nickname} wanna get some #cofe together?", +          "visibility" => "direct" +        }) + +      conn +      |> assign(:user, nil) +      |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +      |> json_response(404) + +      response = +        build_conn() +        |> assign(:user, other_user) +        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") +        |> json_response(200) + +      assert [] == response +    end +  end + +  test "context" do +    user = insert(:user) + +    {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"}) +    {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1}) +    {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2}) +    {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3}) +    {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4}) + +    response = +      build_conn() +      |> assign(:user, nil) +      |> get("/api/v1/statuses/#{id3}/context") +      |> json_response(:ok) + +    assert %{ +             "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}], +             "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}] +           } = response +  end +end diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs new file mode 100644 index 000000000..d3652d964 --- /dev/null +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -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 diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 1e9829886..b3acb7a22 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -6,26 +6,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    use Pleroma.Web.ConnCase    alias Ecto.Changeset -  alias Pleroma.Activity -  alias Pleroma.ActivityExpiration    alias Pleroma.Config    alias Pleroma.Notification    alias Pleroma.Object    alias Pleroma.Repo -  alias Pleroma.ScheduledActivity    alias Pleroma.Tests.ObanHelpers    alias Pleroma.User    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.CommonAPI -  alias Pleroma.Web.MastodonAPI.FilterView    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Token -  alias Pleroma.Web.OStatus    alias Pleroma.Web.Push -  import Pleroma.Factory +    import ExUnit.CaptureLog -  import Tesla.Mock +  import Pleroma.Factory    import Swoosh.TestAssertions +  import Tesla.Mock    @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" @@ -37,462 +33,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    clear_config([:instance, :public])    clear_config([:rich_media, :enabled]) -  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, 200)) - -    {:ok, user} = User.follow(user, following) - -    conn = -      build_conn() -      |> assign(:user, user) -      |> get("/api/v1/timelines/home") - -    assert [%{"content" => "test"}] = json_response(conn, 200) -  end - -  test "the public timeline", %{conn: conn} do -    following = insert(:user) - -    capture_log(fn -> -      {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - -      {:ok, [_activity]} = -        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - -      conn = -        conn -        |> get("/api/v1/timelines/public", %{"local" => "False"}) - -      assert length(json_response(conn, 200)) == 2 - -      conn = -        build_conn() -        |> get("/api/v1/timelines/public", %{"local" => "True"}) - -      assert [%{"content" => "test"}] = json_response(conn, 200) - -      conn = -        build_conn() -        |> get("/api/v1/timelines/public", %{"local" => "1"}) - -      assert [%{"content" => "test"}] = json_response(conn, 200) -    end) -  end - -  test "the public timeline when public is set to false", %{conn: conn} do -    Config.put([:instance, :public], false) - -    assert conn -           |> get("/api/v1/timelines/public", %{"local" => "False"}) -           |> json_response(403) == %{"error" => "This resource requires authentication."} -  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 - -  describe "posting statuses" do -    setup do -      user = insert(:user) - -      conn = -        build_conn() -        |> assign(:user, user) - -      [conn: conn] -    end - -    test "posting a status", %{conn: conn} do -      idempotency_key = "Pikachu rocks!" - -      conn_one = -        conn -        |> put_req_header("idempotency-key", idempotency_key) -        |> post("/api/v1/statuses", %{ -          "status" => "cofe", -          "spoiler_text" => "2hu", -          "sensitive" => "false" -        }) - -      {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key) -      # Six hours -      assert ttl > :timer.seconds(6 * 60 * 60 - 1) - -      assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = -               json_response(conn_one, 200) - -      assert Activity.get_by_id(id) - -      conn_two = -        conn -        |> put_req_header("idempotency-key", idempotency_key) -        |> post("/api/v1/statuses", %{ -          "status" => "cofe", -          "spoiler_text" => "2hu", -          "sensitive" => "false" -        }) - -      assert %{"id" => second_id} = json_response(conn_two, 200) -      assert id == second_id - -      conn_three = -        conn -        |> post("/api/v1/statuses", %{ -          "status" => "cofe", -          "spoiler_text" => "2hu", -          "sensitive" => "false" -        }) - -      assert %{"id" => third_id} = json_response(conn_three, 200) -      refute id == third_id - -      # An activity that will expire: -      # 2 hours -      expires_in = 120 * 60 - -      conn_four = -        conn -        |> post("api/v1/statuses", %{ -          "status" => "oolong", -          "expires_in" => expires_in -        }) - -      assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200) -      assert activity = Activity.get_by_id(fourth_id) -      assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) - -      estimated_expires_at = -        NaiveDateTime.utc_now() -        |> NaiveDateTime.add(expires_in) -        |> NaiveDateTime.truncate(:second) - -      # This assert will fail if the test takes longer than a minute. I sure hope it never does: -      assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 - -      assert fourth_response["pleroma"]["expires_at"] == -               NaiveDateTime.to_iso8601(expiration.scheduled_at) -    end - -    test "replying to a status", %{conn: conn} do -      user = insert(:user) -      {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"}) - -      conn = -        conn -        |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - -      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) - -      activity = Activity.get_by_id(id) - -      assert activity.data["context"] == replied_to.data["context"] -      assert Activity.get_in_reply_to_activity(activity).id == replied_to.id -    end - -    test "replying to a direct message with visibility other than direct", %{conn: conn} do -      user = insert(:user) -      {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"}) - -      Enum.each(["public", "private", "unlisted"], fn visibility -> -        conn = -          conn -          |> post("/api/v1/statuses", %{ -            "status" => "@#{user.nickname} hey", -            "in_reply_to_id" => replied_to.id, -            "visibility" => visibility -          }) - -        assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"} -      end) -    end - -    test "posting a status with an invalid in_reply_to_id", %{conn: conn} do -      conn = -        conn -        |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) - -      assert %{"content" => "xD", "id" => id} = json_response(conn, 200) -      assert Activity.get_by_id(id) -    end - -    test "posting a sensitive status", %{conn: conn} do -      conn = -        conn -        |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) - -      assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) -      assert Activity.get_by_id(id) -    end - -    test "posting a fake status", %{conn: conn} do -      real_conn = -        conn -        |> post("/api/v1/statuses", %{ -          "status" => -            "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" -        }) - -      real_status = json_response(real_conn, 200) - -      assert real_status -      assert Object.get_by_ap_id(real_status["uri"]) - -      real_status = -        real_status -        |> Map.put("id", nil) -        |> Map.put("url", nil) -        |> Map.put("uri", nil) -        |> Map.put("created_at", nil) -        |> Kernel.put_in(["pleroma", "conversation_id"], nil) - -      fake_conn = -        conn -        |> post("/api/v1/statuses", %{ -          "status" => -            "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", -          "preview" => true -        }) - -      fake_status = json_response(fake_conn, 200) - -      assert fake_status -      refute Object.get_by_ap_id(fake_status["uri"]) - -      fake_status = -        fake_status -        |> Map.put("id", nil) -        |> Map.put("url", nil) -        |> Map.put("uri", nil) -        |> Map.put("created_at", nil) -        |> Kernel.put_in(["pleroma", "conversation_id"], nil) - -      assert real_status == fake_status -    end - -    test "posting a status with OGP link preview", %{conn: conn} do -      Config.put([:rich_media, :enabled], true) - -      conn = -        conn -        |> post("/api/v1/statuses", %{ -          "status" => "https://example.com/ogp" -        }) - -      assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200) -      assert Activity.get_by_id(id) -    end - -    test "posting a direct status", %{conn: conn} do -      user2 = insert(:user) -      content = "direct cofe @#{user2.nickname}" - -      conn = -        conn -        |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) - -      assert %{"id" => id} = response = json_response(conn, 200) -      assert response["visibility"] == "direct" -      assert response["pleroma"]["direct_conversation_id"] -      assert activity = Activity.get_by_id(id) -      assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id] -      assert activity.data["to"] == [user2.ap_id] -      assert activity.data["cc"] == [] -    end -  end - -  describe "posting polls" do -    test "posting a poll", %{conn: conn} do -      user = insert(:user) -      time = NaiveDateTime.utc_now() - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "Who is the #bestgrill?", -          "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} -        }) - -      response = json_response(conn, 200) - -      assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> -               title in ["Rei", "Asuka", "Misato"] -             end) - -      assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 -      refute response["poll"]["expred"] -    end - -    test "option limit is enforced", %{conn: conn} do -      user = insert(:user) -      limit = Config.get([:instance, :poll_limits, :max_options]) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "desu~", -          "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} -        }) - -      %{"error" => error} = json_response(conn, 422) -      assert error == "Poll can't contain more than #{limit} options" -    end - -    test "option character limit is enforced", %{conn: conn} do -      user = insert(:user) -      limit = Config.get([:instance, :poll_limits, :max_option_chars]) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "...", -          "poll" => %{ -            "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], -            "expires_in" => 1 -          } -        }) - -      %{"error" => error} = json_response(conn, 422) -      assert error == "Poll options cannot be longer than #{limit} characters each" -    end - -    test "minimal date limit is enforced", %{conn: conn} do -      user = insert(:user) -      limit = Config.get([:instance, :poll_limits, :min_expiration]) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "imagine arbitrary limits", -          "poll" => %{ -            "options" => ["this post was made by pleroma gang"], -            "expires_in" => limit - 1 -          } -        }) - -      %{"error" => error} = json_response(conn, 422) -      assert error == "Expiration date is too soon" -    end - -    test "maximum date limit is enforced", %{conn: conn} do -      user = insert(:user) -      limit = Config.get([:instance, :poll_limits, :max_expiration]) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "imagine arbitrary limits", -          "poll" => %{ -            "options" => ["this post was made by pleroma gang"], -            "expires_in" => limit + 1 -          } -        }) - -      %{"error" => error} = json_response(conn, 422) -      assert error == "Expiration date is too far in the future" -    end -  end - -  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, 200) - -    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, 200) - -    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, 200) - -    # 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, 200) -    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, 200) - -    assert status["url"] != direct.data["id"] -  end -    test "Conversations", %{conn: conn} do      user_one = insert(:user)      user_two = insert(:user) @@ -556,33 +96,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)    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, 200) -    assert status["id"] == direct.id -  end -    test "verify_credentials", %{conn: conn} do      user = insert(:user) @@ -752,407 +265,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert expected == json_response(conn, 200)    end -  test "get a status", %{conn: conn} do -    activity = insert(:note_activity) - -    conn = -      conn -      |> get("/api/v1/statuses/#{activity.id}") - -    assert %{"id" => id} = json_response(conn, 200) -    assert id == to_string(activity.id) -  end - -  test "get statuses by IDs", %{conn: conn} do -    %{id: id1} = insert(:note_activity) -    %{id: id2} = insert(:note_activity) - -    query_string = "ids[]=#{id1}&ids[]=#{id2}" -    conn = get(conn, "/api/v1/statuses/?#{query_string}") - -    assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"]) -  end - -  describe "deleting a status" do -    test "when you created it", %{conn: conn} do -      activity = insert(:note_activity) -      author = User.get_cached_by_ap_id(activity.data["actor"]) - -      conn = -        conn -        |> assign(:user, author) -        |> delete("/api/v1/statuses/#{activity.id}") - -      assert %{} = json_response(conn, 200) - -      refute Activity.get_by_id(activity.id) -    end - -    test "when you didn't create it", %{conn: conn} do -      activity = insert(:note_activity) -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> delete("/api/v1/statuses/#{activity.id}") - -      assert %{"error" => _} = json_response(conn, 403) - -      assert Activity.get_by_id(activity.id) == activity -    end - -    test "when you're an admin or moderator", %{conn: conn} do -      activity1 = insert(:note_activity) -      activity2 = insert(:note_activity) -      admin = insert(:user, info: %{is_admin: true}) -      moderator = insert(:user, info: %{is_moderator: true}) - -      res_conn = -        conn -        |> assign(:user, admin) -        |> delete("/api/v1/statuses/#{activity1.id}") - -      assert %{} = json_response(res_conn, 200) - -      res_conn = -        conn -        |> assign(:user, moderator) -        |> delete("/api/v1/statuses/#{activity2.id}") - -      assert %{} = json_response(res_conn, 200) - -      refute Activity.get_by_id(activity1.id) -      refute Activity.get_by_id(activity2.id) -    end -  end - -  describe "filters" do -    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 - -  describe "list timelines" 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, 200) - -      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, 200) - -      assert id == to_string(activity_one.id) -    end -  end - -  describe "reblogging" do -    test "reblogs and returns the reblogged status", %{conn: conn} do -      activity = insert(:note_activity) -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{activity.id}/reblog") - -      assert %{ -               "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, -               "reblogged" => true -             } = json_response(conn, 200) - -      assert to_string(activity.id) == id -    end - -    test "reblogged status for another user", %{conn: conn} do -      activity = insert(:note_activity) -      user1 = insert(:user) -      user2 = insert(:user) -      user3 = insert(:user) -      CommonAPI.favorite(activity.id, user2) -      {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) -      {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1) -      {:ok, _, _object} = CommonAPI.repeat(activity.id, user2) - -      conn_res = -        conn -        |> assign(:user, user3) -        |> get("/api/v1/statuses/#{reblog_activity1.id}") - -      assert %{ -               "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, -               "reblogged" => false, -               "favourited" => false, -               "bookmarked" => false -             } = json_response(conn_res, 200) - -      conn_res = -        conn -        |> assign(:user, user2) -        |> get("/api/v1/statuses/#{reblog_activity1.id}") - -      assert %{ -               "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, -               "reblogged" => true, -               "favourited" => true, -               "bookmarked" => true -             } = json_response(conn_res, 200) - -      assert to_string(activity.id) == id -    end - -    test "returns 400 error when activity is not exist", %{conn: conn} do -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/foo/reblog") - -      assert json_response(conn, 400) == %{"error" => "Could not repeat"} -    end -  end - -  describe "unreblogging" do -    test "unreblogs and returns the unreblogged status", %{conn: conn} do -      activity = insert(:note_activity) -      user = insert(:user) - -      {:ok, _, _} = CommonAPI.repeat(activity.id, user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{activity.id}/unreblog") - -      assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200) - -      assert to_string(activity.id) == id -    end - -    test "returns 400 error when activity is not exist", %{conn: conn} do -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/foo/unreblog") - -      assert json_response(conn, 400) == %{"error" => "Could not unrepeat"} -    end -  end - -  describe "favoriting" do -    test "favs a status and returns it", %{conn: conn} do -      activity = insert(:note_activity) -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{activity.id}/favourite") - -      assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = -               json_response(conn, 200) - -      assert to_string(activity.id) == id -    end - -    test "returns 400 error for a wrong id", %{conn: conn} do -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/1/favourite") - -      assert json_response(conn, 400) == %{"error" => "Could not favorite"} -    end -  end - -  describe "unfavoriting" do -    test "unfavorites a status and returns it", %{conn: conn} do -      activity = insert(:note_activity) -      user = insert(:user) - -      {:ok, _, _} = CommonAPI.favorite(activity.id, user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{activity.id}/unfavourite") - -      assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = -               json_response(conn, 200) - -      assert to_string(activity.id) == id -    end - -    test "returns 400 error for a wrong id", %{conn: conn} do -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/1/unfavourite") - -      assert json_response(conn, 400) == %{"error" => "Could not unfavorite"} -    end -  end -    describe "user timelines" do      test "gets a users statuses", %{conn: conn} do        user_one = insert(:user) @@ -1338,51 +450,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    end    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 "verify_credentials", %{conn: conn} do        user = insert(:user, %{info: %User.Info{default_scope: "private"}}) @@ -1394,28 +461,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)        assert id == to_string(user.id)      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    describe "account fetching" do @@ -1567,62 +612,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      end    end -  test "hashtag timeline", %{conn: conn} do -    following = insert(:user) - -    capture_log(fn -> -      {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"}) - -      {:ok, [_activity]} = -        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - -      nconn = -        conn -        |> get("/api/v1/timelines/tag/2hu") - -      assert [%{"id" => id}] = json_response(nconn, 200) - -      assert id == to_string(activity.id) - -      # works for different capitalization too -      nconn = -        conn -        |> get("/api/v1/timelines/tag/2HU") - -      assert [%{"id" => id}] = json_response(nconn, 200) - -      assert id == to_string(activity.id) -    end) -  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 = -      conn -      |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]}) - -    [status_none, status_test1, status_test] = json_response(any_test, 200) - -    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 = -      conn -      |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]}) - -    assert [status_test1] == json_response(restricted_test, 200) - -    all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]}) - -    assert [status_none] == json_response(all_test, 200) -  end -    test "getting followers", %{conn: conn} do      user = insert(:user)      other_user = insert(:user) @@ -2030,46 +1019,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert [%{"id" => ^other_user_id}] = json_response(conn, 200)    end -  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 -    test "unimplemented follow_requests, blocks, domain blocks" do      user = insert(:user) @@ -2416,10 +1365,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        [user: user, activity: activity]      end -    clear_config([:instance, :max_pinned_statuses]) do -      Config.put([:instance, :max_pinned_statuses], 1) -    end -      test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do        {:ok, _} = CommonAPI.pin(activity.id, user) @@ -2433,288 +1378,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert [%{"id" => ^id_str, "pinned" => true}] = result      end - -    test "pin status", %{conn: conn, user: user, activity: activity} do -      id_str = to_string(activity.id) - -      assert %{"id" => ^id_str, "pinned" => true} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{activity.id}/pin") -               |> json_response(200) - -      assert [%{"id" => ^id_str, "pinned" => true}] = -               conn -               |> assign(:user, user) -               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") -               |> json_response(200) -    end - -    test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do -      {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"}) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{dm.id}/pin") - -      assert json_response(conn, 400) == %{"error" => "Could not pin"} -    end - -    test "unpin status", %{conn: conn, user: user, activity: activity} do -      {:ok, _} = CommonAPI.pin(activity.id, user) - -      id_str = to_string(activity.id) -      user = refresh_record(user) - -      assert %{"id" => ^id_str, "pinned" => false} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{activity.id}/unpin") -               |> json_response(200) - -      assert [] = -               conn -               |> assign(:user, user) -               |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") -               |> json_response(200) -    end - -    test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/1/unpin") - -      assert json_response(conn, 400) == %{"error" => "Could not unpin"} -    end - -    test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do -      {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) - -      id_str_one = to_string(activity_one.id) - -      assert %{"id" => ^id_str_one, "pinned" => true} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{id_str_one}/pin") -               |> json_response(200) - -      user = refresh_record(user) - -      assert %{"error" => "You have already pinned the maximum number of statuses"} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{activity_two.id}/pin") -               |> json_response(400) -    end -  end - -  describe "cards" do -    setup do -      Config.put([:rich_media, :enabled], true) - -      user = insert(:user) -      %{user: user} -    end - -    test "returns empty result when rich_media disabled", %{conn: conn, user: user} do -      Config.put([:rich_media, :enabled], false) -      {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"}) - -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/card") -        |> json_response(200) - -      assert response == nil -    end - -    test "returns rich-media card", %{conn: conn, user: user} do -      {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"}) - -      card_data = %{ -        "image" => "http://ia.media-imdb.com/images/rock.jpg", -        "provider_name" => "example.com", -        "provider_url" => "https://example.com", -        "title" => "The Rock", -        "type" => "link", -        "url" => "https://example.com/ogp", -        "description" => -          "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", -        "pleroma" => %{ -          "opengraph" => %{ -            "image" => "http://ia.media-imdb.com/images/rock.jpg", -            "title" => "The Rock", -            "type" => "video.movie", -            "url" => "https://example.com/ogp", -            "description" => -              "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." -          } -        } -      } - -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/card") -        |> json_response(200) - -      assert response == card_data - -      # works with private posts -      {:ok, activity} = -        CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"}) - -      response_two = -        conn -        |> assign(:user, user) -        |> get("/api/v1/statuses/#{activity.id}/card") -        |> json_response(200) - -      assert response_two == card_data -    end - -    test "replaces missing description with an empty string", %{conn: conn, user: user} do -      {:ok, activity} = -        CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"}) - -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/card") -        |> json_response(:ok) - -      assert response == %{ -               "type" => "link", -               "title" => "Pleroma", -               "description" => "", -               "image" => nil, -               "provider_name" => "example.com", -               "provider_url" => "https://example.com", -               "url" => "https://example.com/ogp-missing-data", -               "pleroma" => %{ -                 "opengraph" => %{ -                   "title" => "Pleroma", -                   "type" => "website", -                   "url" => "https://example.com/ogp-missing-data" -                 } -               } -             } -    end - -    test "returns 404 response when id invalid", %{conn: conn} do -      assert %{"error" => "Record not found"} = -               conn -               |> get("/api/v1/statuses/9eoozpwTul5mjSEDRI/card") -               |> json_response(404) -    end - -    test "returns 404 response when id isn't FlakeID", %{conn: conn} do -      assert %{"error" => "Record not found"} = -               conn -               |> get("/api/v1/statuses/3ebbadd1-eb14-4e20-8118/card") -               |> json_response(404) - -      assert %{"error" => "Record not found"} = -               conn -               |> get("/api/v1/statuses/8118/card") -               |> json_response(404) -    end -  end - -  test "bookmarks" do -    user = insert(:user) -    for_user = insert(:user) - -    {:ok, activity1} = -      CommonAPI.post(user, %{ -        "status" => "heweoo?" -      }) - -    {:ok, activity2} = -      CommonAPI.post(user, %{ -        "status" => "heweoo!" -      }) - -    response1 = -      build_conn() -      |> assign(:user, for_user) -      |> post("/api/v1/statuses/#{activity1.id}/bookmark") - -    assert json_response(response1, 200)["bookmarked"] == true - -    response2 = -      build_conn() -      |> assign(:user, for_user) -      |> post("/api/v1/statuses/#{activity2.id}/bookmark") - -    assert json_response(response2, 200)["bookmarked"] == true - -    bookmarks = -      build_conn() -      |> assign(:user, for_user) -      |> get("/api/v1/bookmarks") - -    assert [json_response(response2, 200), json_response(response1, 200)] == -             json_response(bookmarks, 200) - -    response1 = -      build_conn() -      |> assign(:user, for_user) -      |> post("/api/v1/statuses/#{activity1.id}/unbookmark") - -    assert json_response(response1, 200)["bookmarked"] == false - -    bookmarks = -      build_conn() -      |> assign(:user, for_user) -      |> get("/api/v1/bookmarks") - -    assert [json_response(response2, 200)] == json_response(bookmarks, 200) -  end - -  describe "conversation muting" do -    setup do -      post_user = insert(:user) -      user = insert(:user) - -      {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"}) - -      [user: user, activity: activity] -    end - -    test "mute conversation", %{conn: conn, user: user, activity: activity} do -      id_str = to_string(activity.id) - -      assert %{"id" => ^id_str, "muted" => true} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{activity.id}/mute") -               |> json_response(200) -    end - -    test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do -      {:ok, _} = CommonAPI.add_mute(user, activity) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses/#{activity.id}/mute") - -      assert json_response(conn, 400) == %{"error" => "conversation is already muted"} -    end - -    test "unmute conversation", %{conn: conn, user: user, activity: activity} do -      {:ok, _} = CommonAPI.add_mute(user, activity) - -      id_str = to_string(activity.id) -      user = refresh_record(user) - -      assert %{"id" => ^id_str, "muted" => false} = -               conn -               |> assign(:user, user) -               |> post("/api/v1/statuses/#{activity.id}/unmute") -               |> json_response(200) -    end    end    describe "reports" do @@ -2938,18 +1601,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert conn.status == 302        assert redirected_to(conn) == path      end -  end - -  describe "GET /web/login" do -    test "redirects to /oauth/authorize", %{conn: conn} do -      app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") -      conn = get(conn, "/web/login", %{}) - -      assert conn.status == 302 - -      assert redirected_to(conn) == -               "/oauth/authorize?response_type=code&client_id=#{app.client_id}&redirect_uri=.&scope=read+write+follow+push" -    end      test "redirects to the getting-started page when referer is not present", %{conn: conn} do        app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") @@ -2960,273 +1611,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do        assert conn.status == 302        assert redirected_to(conn) == "/web/getting-started"      end - -    test "redirects to the getting-started page when user assigned", %{conn: conn} do -      user = insert(:user) - -      conn = -        conn -        |> assign(:user, user) -        |> get("/web/login", %{}) - -      assert conn.status == 302 -      assert redirected_to(conn) == "/web/getting-started" -    end -  end - -  describe "scheduled activities" do -    test "creates a scheduled activity", %{conn: conn} do -      user = insert(:user) -      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "scheduled", -          "scheduled_at" => scheduled_at -        }) - -      assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200) -      assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(scheduled_at) -      assert [] == Repo.all(Activity) -    end - -    test "creates a scheduled activity with a media attachment", %{conn: conn} do -      user = insert(:user) -      scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) - -      file = %Plug.Upload{ -        content_type: "image/jpg", -        path: Path.absname("test/fixtures/image.jpg"), -        filename: "an_image.jpg" -      } - -      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "media_ids" => [to_string(upload.id)], -          "status" => "scheduled", -          "scheduled_at" => scheduled_at -        }) - -      assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200) -      assert %{"type" => "image"} = media_attachment -    end - -    test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", -         %{conn: conn} do -      user = insert(:user) - -      scheduled_at = -        NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{ -          "status" => "not scheduled", -          "scheduled_at" => scheduled_at -        }) - -      assert %{"content" => "not scheduled"} = json_response(conn, 200) -      assert [] == Repo.all(ScheduledActivity) -    end - -    test "returns error when daily user limit is exceeded", %{conn: conn} do -      user = insert(:user) - -      today = -        NaiveDateTime.utc_now() -        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) -        |> NaiveDateTime.to_iso8601() - -      attrs = %{params: %{}, scheduled_at: today} -      {:ok, _} = ScheduledActivity.create(user, attrs) -      {:ok, _} = ScheduledActivity.create(user, attrs) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) - -      assert %{"error" => "daily limit exceeded"} == json_response(conn, 422) -    end - -    test "returns error when total user limit is exceeded", %{conn: conn} do -      user = insert(:user) - -      today = -        NaiveDateTime.utc_now() -        |> NaiveDateTime.add(:timer.minutes(6), :millisecond) -        |> NaiveDateTime.to_iso8601() - -      tomorrow = -        NaiveDateTime.utc_now() -        |> NaiveDateTime.add(:timer.hours(36), :millisecond) -        |> NaiveDateTime.to_iso8601() - -      attrs = %{params: %{}, scheduled_at: today} -      {:ok, _} = ScheduledActivity.create(user, attrs) -      {:ok, _} = ScheduledActivity.create(user, attrs) -      {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) - -      conn = -        conn -        |> assign(:user, user) -        |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) - -      assert %{"error" => "total limit exceeded"} == json_response(conn, 422) -    end - -    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 - -  test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do -    user1 = insert(:user) -    user2 = insert(:user) -    user3 = insert(:user) - -    {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"}) - -    # Reply to status from another user -    conn1 = -      conn -      |> assign(:user, user2) -      |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) - -    assert %{"content" => "xD", "id" => id} = json_response(conn1, 200) - -    activity = Activity.get_by_id_with_object(id) - -    assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"] -    assert Activity.get_in_reply_to_activity(activity).id == replied_to.id - -    # Reblog from the third user -    conn2 = -      conn -      |> assign(:user, user3) -      |> post("/api/v1/statuses/#{activity.id}/reblog") - -    assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = -             json_response(conn2, 200) - -    assert to_string(activity.id) == id - -    # Getting third user status -    conn3 = -      conn -      |> assign(:user, user3) -      |> get("api/v1/timelines/home") - -    [reblogged_activity] = json_response(conn3, 200) - -    assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id - -    replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) -    assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id    end    describe "create account by app" do @@ -3567,197 +1951,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      end    end -  describe "GET /api/v1/statuses/:id/favourited_by" do -    setup do -      user = insert(:user) -      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) - -      conn = -        build_conn() -        |> assign(:user, user) - -      [conn: conn, activity: activity, user: user] -    end - -    test "returns users who have favorited the status", %{conn: conn, activity: activity} do -      other_user = insert(:user) -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) - -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/favourited_by") -        |> json_response(:ok) - -      [%{"id" => id}] = response - -      assert id == other_user.id -    end - -    test "returns empty array when status has not been favorited yet", %{ -      conn: conn, -      activity: activity -    } do -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/favourited_by") -        |> json_response(:ok) - -      assert Enum.empty?(response) -    end - -    test "does not return users who have favorited the status but are blocked", %{ -      conn: %{assigns: %{user: user}} = conn, -      activity: activity -    } do -      other_user = insert(:user) -      {:ok, user} = User.block(user, other_user) - -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) - -      response = -        conn -        |> assign(:user, user) -        |> get("/api/v1/statuses/#{activity.id}/favourited_by") -        |> json_response(:ok) - -      assert Enum.empty?(response) -    end - -    test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do -      other_user = insert(:user) -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) - -      response = -        conn -        |> assign(:user, nil) -        |> get("/api/v1/statuses/#{activity.id}/favourited_by") -        |> json_response(:ok) - -      [%{"id" => id}] = response -      assert id == other_user.id -    end - -    test "requires authentification for private posts", %{conn: conn, user: user} do -      other_user = insert(:user) - -      {:ok, activity} = -        CommonAPI.post(user, %{ -          "status" => "@#{other_user.nickname} wanna get some #cofe together?", -          "visibility" => "direct" -        }) - -      {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) - -      conn -      |> assign(:user, nil) -      |> get("/api/v1/statuses/#{activity.id}/favourited_by") -      |> json_response(404) - -      response = -        build_conn() -        |> assign(:user, other_user) -        |> get("/api/v1/statuses/#{activity.id}/favourited_by") -        |> json_response(200) - -      [%{"id" => id}] = response -      assert id == other_user.id -    end -  end - -  describe "GET /api/v1/statuses/:id/reblogged_by" do -    setup do -      user = insert(:user) -      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) - -      conn = -        build_conn() -        |> assign(:user, user) - -      [conn: conn, activity: activity, user: user] -    end - -    test "returns users who have reblogged the status", %{conn: conn, activity: activity} do -      other_user = insert(:user) -      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) - -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -        |> json_response(:ok) - -      [%{"id" => id}] = response - -      assert id == other_user.id -    end - -    test "returns empty array when status has not been reblogged yet", %{ -      conn: conn, -      activity: activity -    } do -      response = -        conn -        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -        |> json_response(:ok) - -      assert Enum.empty?(response) -    end - -    test "does not return users who have reblogged the status but are blocked", %{ -      conn: %{assigns: %{user: user}} = conn, -      activity: activity -    } do -      other_user = insert(:user) -      {:ok, user} = User.block(user, other_user) - -      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) - -      response = -        conn -        |> assign(:user, user) -        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -        |> json_response(:ok) - -      assert Enum.empty?(response) -    end - -    test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do -      other_user = insert(:user) -      {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) - -      response = -        conn -        |> assign(:user, nil) -        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -        |> json_response(:ok) - -      [%{"id" => id}] = response -      assert id == other_user.id -    end - -    test "requires authentification for private posts", %{conn: conn, user: user} do -      other_user = insert(:user) - -      {:ok, activity} = -        CommonAPI.post(user, %{ -          "status" => "@#{other_user.nickname} wanna get some #cofe together?", -          "visibility" => "direct" -        }) - -      conn -      |> assign(:user, nil) -      |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -      |> json_response(404) - -      response = -        build_conn() -        |> assign(:user, other_user) -        |> get("/api/v1/statuses/#{activity.id}/reblogged_by") -        |> json_response(200) - -      assert [] == response -    end -  end -    describe "POST /auth/password, with valid parameters" do      setup %{conn: conn} do        user = insert(:user) diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs index 7fcb2bd55..848fce7ad 100644 --- a/test/web/mastodon_api/mastodon_api_test.exs +++ b/test/web/mastodon_api/mastodon_api_test.exs @@ -75,9 +75,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest 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) diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index 9231aaec8..86268fcfa 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest 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)      } diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 51f8434fa..c17d0ef95 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do      {: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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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 diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 7eaeda4a0..7eaeda4a0 100644 --- a/test/web/pleroma_api/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs diff --git a/test/web/pleroma_api/controllers/subscription_notification_controller_test.exs b/test/web/pleroma_api/controllers/subscription_notification_controller_test.exs new file mode 100644 index 000000000..c6a71732d --- /dev/null +++ b/test/web/pleroma_api/controllers/subscription_notification_controller_test.exs @@ -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  | 
