Merge branch 'refactor/subscription' into 'develop'
authorkaniini <ariadne@dereferenced.org>
Fri, 27 Sep 2019 03:51:24 +0000 (03:51 +0000)
committerkaniini <ariadne@dereferenced.org>
Fri, 27 Sep 2019 03:51:24 +0000 (03:51 +0000)
Refactor subscription functionality

Closes #1130

See merge request pleroma/pleroma!1664

20 files changed:
CHANGELOG.md
docs/api/admin_api.md
docs/api/pleroma_api.md
docs/installation/alpine_linux_en.md
docs/installation/debian_based_jp.md
lib/pleroma/moderation_log.ex
lib/pleroma/user/info.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/moderation_log_view.ex
lib/pleroma/web/common_api/activity_draft.ex [new file with mode: 0644]
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/controller_helper.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
test/moderation_log_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/mastodon_api/controllers/timeline_controller_test.exs [new file with mode: 0644]
test/web/mastodon_api/mastodon_api_controller_test.exs

index 291c961ee0e2746c4743747680072db8a73c4b24..0a8163135ca82594ce46388e64b7d83ba0f2ed76 100644 (file)
@@ -51,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
@@ -117,6 +116,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - 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.
+- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
 
 ### Changed
 - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
@@ -124,6 +124,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
index d4e08f221d4fb1798921c40ad19306e737b9839c..fcdb339444f3fc70839f1ba0cd7f5abe67540745 100644 (file)
@@ -711,6 +711,7 @@ Compile time settings (need instance reboot):
     }
   ]
 }
+```
 
 - Response:
 
@@ -731,7 +732,11 @@ Compile time settings (need instance reboot):
 - Method `GET`
 - Params:
   - *optional* `page`: **integer** page number
-  - *optional* `page_size`: **integer** number of users per page (default is `50`)
+  - *optional* `page_size`: **integer** number of log entries per page (default is `50`)
+  - *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42`
+  - *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42
+  - *optional* `user_id`: **integer** filter logs by actor's id
+  - *optional* `search`: **string** search logs by the log message
 - Response:
 
 ```json
index a469ddfbf3551f01a43d4c8601e845adccaedaff..ac5489aa3e9dd73eafbaaf48368275b92fc2ba8e 100644 (file)
@@ -423,6 +423,15 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
 * Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
   errors downloading the pack
 
+## `POST /api/pleroma/emoji/packs/list_from`
+### Requests the instance to list the packs from another instance
+* Method `POST`
+* Authentication: required
+* Params:
+  * `instance_address`: the address of the instance to download from
+* Response: JSON with the pack list, same as if the request was made to that instance's
+  list endpoint directly + 200 status
+
 ## `GET /api/pleroma/emoji/packs/:name/download_shared`
 ### Requests a local pack from the instance
 * Method `GET`
index 1f300f353403d78d2b0891cc93ee2fa1956a498a..f200362ca046885fe8a280ce6a689d2a22e875e3 100644 (file)
@@ -1,7 +1,9 @@
 # Installing on Alpine Linux
 ## Installation
 
-This guide is a step-by-step installation guide for Alpine Linux. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -l <username> -s $SHELL -c 'command'` instead.
+This guide is a step-by-step installation guide for Alpine Linux. The instructions were verified against Alpine v3.10 standard image. You might miss additional dependencies if you use `netboot` instead.
+
+It assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -l <username> -s $SHELL -c 'command'` instead.
 
 ### Required packages
 
@@ -20,12 +22,13 @@ This guide is a step-by-step installation guide for Alpine Linux. It also assume
 
 ### Prepare the system
 
-* First make sure to have the community repository enabled:
+* The community repository must be enabled in `/etc/apk/repositories`. Depending on which version and mirror you use this looks like `http://alpine.42.fr/v3.10/community`. If you autogenerated the mirror during installation:
 
 ```shell
-echo "https://nl.alpinelinux.org/alpine/latest-stable/community" | sudo tee -a /etc/apk/repository
+awk 'NR==2' /etc/apk/repositories | sed 's/main/community/' | tee -a /etc/apk/repositories
 ```
 
+
 * Then update the system, if not already done:
 
 ```shell
@@ -77,7 +80,8 @@ sudo rc-update add postgresql
 * Add a new system user for the Pleroma service:
 
 ```shell
-sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma
+sudo addgroup pleroma
+sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
 ```
 
 **Note**: To execute a single command as the Pleroma system user, use `sudo -Hu pleroma command`. You can also switch to a shell by using `sudo -Hu pleroma $SHELL`. If you 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
index caf72363b6398573801c8563b428fe3ad0ea52d0..5ca6b36344f577d86786fa3b436e92ba2a0ba89e 100644 (file)
 
 ## インストール
 
-このガイドは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を入力してください。
-    * この処理には時間がかかります。私もよく分かりませんが、何らかのコンパイルが行われているようです。
-    * ã\81\82ã\81ªã\81\9fã\81®ã\82¤ã\83³ã\82¹ã\82¿ã\83³ã\82¹ã\81«ã\81¤ã\81\84ã\81¦ã\80\81ã\81\84ã\81\8fã\81¤ã\81\8bã\81®è³ªå\95\8fã\81\8cã\81\82ã\82\8aã\81¾ã\81\99ã\80\82ã\81\9dã\81®å\9b\9eç­\94ã\81¯ `config/generated_config.exs` ã\81¨ã\81\84ã\81\86ã\82³ã\83³ã\83\95ã\82£ã\82®ã\83¥ã\83¬ã\83¼ã\82·ã\83§ã\83³ã\83\95ã\82¡ã\82¤ã\83«ã\81«ä¿\9då­\98されます。
+    * このときにpleromaの一部がコンパイルされるため、この処理には時間がかかります。
+    * ã\81\82ã\81ªã\81\9fã\81®ã\82¤ã\83³ã\82¹ã\82¿ã\83³ã\82¹ã\81«ã\81¤ã\81\84ã\81¦ã\80\81ã\81\84ã\81\8fã\81¤ã\81\8bã\81®è³ªå\95\8fã\81\95ã\82\8cã\81¾ã\81\99ã\80\82ã\81\93ã\81®è³ªå\95\8fã\81«ã\82\88ã\82\8a `config/generated_config.exs` ã\81¨ã\81\84ã\81\86設å®\9aã\83\95ã\82¡ã\82¤ã\83«ã\81\8cç\94\9fæ\88\90されます。
 
-**注意**: メディアプロクシを有効にすると回答して、なおかつ、キャッシュの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
 ```
 
-* ã\81\9dã\81\97ã\81¦ã\80\81ã\83\87ã\83¼ã\82¿ã\83\99ã\83¼ã\82¹ã\81®ã\83\9fグレーションを実行します。
+* ã\81\9dã\81\97ã\81¦ã\80\81ã\83\87ã\83¼ã\82¿ã\83\99ã\83¼ã\82¹ã\81®ã\83\9eã\82¤グレーションを実行します。
 ```
-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
 ```
 
-### ã\82¤ã\83³ã\82¹ã\83\88ã\83¼ã\83«ã\82\92çµ\82ã\82\8fã\82\89ã\81\9bã\82\8b
+### ã\82¤ã\83³ã\82¹ã\83\88ã\83¼ã\83«ã\81®æ\9c\80çµ\82段é\9a\8e
 
-あなたの新しいインスタンスを世界に向けて公開するには、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ã\82³ã\83³ã\83\95ã\82£ã\82®ã\83¥ã\83¬ã\83¼ã\82·ã\83§ã\83³ã\81®ä¾\8bをnginxフォルダーにコピーします。
+* nginxã\81®è¨­å®\9aã\83\95ã\82¡ã\82¤ã\83«ã\82µã\83³ã\83\97ã\83«を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
+```
 
-#### ã\82³ã\83³ã\83\95ã\82£ã\82®ã\83¥ã\83¬ã\83¼ã\82·ã\83§ã\83³とカスタマイズ
+#### ã\81\9dã\81®ä»\96ã\81®è¨­å®\9aとカスタマイズ
 
 * [Backup your instance](backup.html)
 * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
index 1ef6fe67afe85f7a1b5ea008cfe6dff17ef38c0e..352cad4335a40f9c1beea6ac5c2b258191527b72 100644 (file)
@@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do
     timestamps()
   end
 
-  def get_all(page, page_size) do
-    from(q in __MODULE__,
-      order_by: [desc: q.inserted_at],
+  def get_all(params) do
+    base_query =
+      get_all_query()
+      |> maybe_filter_by_date(params)
+      |> maybe_filter_by_user(params)
+      |> maybe_filter_by_search(params)
+
+    query_with_pagination = base_query |> paginate_query(params)
+
+    %{
+      items: Repo.all(query_with_pagination),
+      count: Repo.aggregate(base_query, :count, :id)
+    }
+  end
+
+  defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
+
+  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
+    from(q in query,
+      where: q.inserted_at >= ^parse_datetime(start_date)
+    )
+  end
+
+  defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
+    from(q in query,
+      where: q.inserted_at <= ^parse_datetime(end_date)
+    )
+  end
+
+  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
+    from(q in query,
+      where: q.inserted_at >= ^parse_datetime(start_date),
+      where: q.inserted_at <= ^parse_datetime(end_date)
+    )
+  end
+
+  defp maybe_filter_by_user(query, %{user_id: nil}), do: query
+
+  defp maybe_filter_by_user(query, %{user_id: user_id}) do
+    from(q in query,
+      where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
+    )
+  end
+
+  defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
+    do: query
+
+  defp maybe_filter_by_search(query, %{search: search}) do
+    from(q in query,
+      where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
+    )
+  end
+
+  defp paginate_query(query, %{page: page, page_size: page_size}) do
+    from(q in query,
       limit: ^page_size,
       offset: ^((page - 1) * page_size)
     )
-    |> Repo.all()
   end
 
+  defp get_all_query do
+    from(q in __MODULE__,
+      order_by: [desc: q.inserted_at]
+    )
+  end
+
+  defp parse_datetime(datetime) do
+    {:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
+
+    parsed_datetime
+  end
+
+  @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         subject: %User{} = subject,
         action: action,
         permission: permission
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        subject: user_to_map(subject),
-        action: action,
-        permission: permission
+        "actor" => user_to_map(actor),
+        "subject" => user_to_map(subject),
+        "action" => action,
+        "permission" => permission,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         action: "report_update",
         subject: %Activity{data: %{"type" => "Flag"}} = subject
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "report_update",
-        subject: report_to_map(subject)
+        "actor" => user_to_map(actor),
+        "action" => "report_update",
+        "subject" => report_to_map(subject),
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         action: "report_response",
         subject: %Activity{} = subject,
         text: text
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "report_response",
-        subject: report_to_map(subject),
-        text: text
+        "actor" => user_to_map(actor),
+        "action" => "report_response",
+        "subject" => report_to_map(subject),
+        "text" => text,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{
+          actor: User,
+          subject: Activity,
+          action: String.t(),
+          sensitive: String.t(),
+          visibility: String.t()
+        }) :: {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         action: "status_update",
@@ -76,41 +158,49 @@ defmodule Pleroma.ModerationLog do
         sensitive: sensitive,
         visibility: visibility
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "status_update",
-        subject: status_to_map(subject),
-        sensitive: sensitive,
-        visibility: visibility
+        "actor" => user_to_map(actor),
+        "action" => "status_update",
+        "subject" => status_to_map(subject),
+        "sensitive" => sensitive,
+        "visibility" => visibility,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         action: "status_delete",
         subject_id: subject_id
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "status_delete",
-        subject_id: subject_id
+        "actor" => user_to_map(actor),
+        "action" => "status_delete",
+        "subject_id" => subject_id,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
   @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
           {:ok, ModerationLog} | {:error, any}
   def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: action,
-        subject: user_to_map(subject)
+        "actor" => user_to_map(actor),
+        "action" => action,
+        "subject" => user_to_map(subject),
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
   @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
@@ -118,97 +208,128 @@ defmodule Pleroma.ModerationLog do
   def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
     subjects = Enum.map(subjects, &user_to_map/1)
 
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: action,
-        subjects: subjects
+        "actor" => user_to_map(actor),
+        "action" => action,
+        "subjects" => subjects,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         followed: %User{} = followed,
         follower: %User{} = follower,
         action: "follow"
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "follow",
-        followed: user_to_map(followed),
-        follower: user_to_map(follower)
+        "actor" => user_to_map(actor),
+        "action" => "follow",
+        "followed" => user_to_map(followed),
+        "follower" => user_to_map(follower),
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         followed: %User{} = followed,
         follower: %User{} = follower,
         action: "unfollow"
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: "unfollow",
-        followed: user_to_map(followed),
-        follower: user_to_map(follower)
+        "actor" => user_to_map(actor),
+        "action" => "unfollow",
+        "followed" => user_to_map(followed),
+        "follower" => user_to_map(follower),
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{
+          actor: User,
+          action: String.t(),
+          nicknames: [String.t()],
+          tags: [String.t()]
+        }) :: {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         nicknames: nicknames,
         tags: tags,
         action: action
       }) do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        nicknames: nicknames,
-        tags: tags,
-        action: action
+        "actor" => user_to_map(actor),
+        "nicknames" => nicknames,
+        "tags" => tags,
+        "action" => action,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
   end
 
+  @spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
+          {:ok, ModerationLog} | {:error, any}
   def insert_log(%{
         actor: %User{} = actor,
         action: action,
         target: target
       })
       when action in ["relay_follow", "relay_unfollow"] do
-    Repo.insert(%ModerationLog{
+    %ModerationLog{
       data: %{
-        actor: user_to_map(actor),
-        action: action,
-        target: target
+        "actor" => user_to_map(actor),
+        "action" => action,
+        "target" => target,
+        "message" => ""
       }
-    })
+    }
+    |> insert_log_entry_with_message()
+  end
+
+  @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
+
+  defp insert_log_entry_with_message(entry) do
+    entry.data["message"]
+    |> put_in(get_log_entry_message(entry))
+    |> Repo.insert()
   end
 
   defp user_to_map(%User{} = user) do
     user
     |> Map.from_struct()
     |> Map.take([:id, :nickname])
-    |> Map.put(:type, "user")
+    |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
+    |> Map.put("type", "user")
   end
 
   defp report_to_map(%Activity{} = report) do
     %{
-      type: "report",
-      id: report.id,
-      state: report.data["state"]
+      "type" => "report",
+      "id" => report.id,
+      "state" => report.data["state"]
     }
   end
 
   defp status_to_map(%Activity{} = status) do
     %{
-      type: "status",
-      id: status.id
+      "type" => "status",
+      "id" => status.id
     }
   end
 
index eef985d0d5fcc6d259fc070c23686936ea5f8106..ebd4ddebf2c68989ebb39b1b1aa3d5ad3fc982f3 100644 (file)
@@ -338,9 +338,7 @@ defmodule Pleroma.User.Info do
     name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
     value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
 
-    is_binary(name) &&
-      is_binary(value) &&
-      String.length(name) <= name_limit &&
+    is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
       String.length(value) <= value_limit
   end
 
index e9a048b9b703517d6e297caf55637effd8ea3c69..90aef99f7857d921e92055f980adb5810fc08bef 100644 (file)
@@ -556,7 +556,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   def list_log(conn, params) do
     {page, page_size} = page_params(params)
 
-    log = ModerationLog.get_all(page, page_size)
+    log =
+      ModerationLog.get_all(%{
+        page: page,
+        page_size: page_size,
+        start_date: params["start_date"],
+        end_date: params["end_date"],
+        user_id: params["user_id"],
+        search: params["search"]
+      })
 
     conn
     |> put_view(ModerationLogView)
index b3fc7cfe57106c9d01fadfd45a42e061bab72bfe..e7752d1f3994e09075aeca18bb297d1969bf4170 100644 (file)
@@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do
   alias Pleroma.ModerationLog
 
   def render("index.json", %{log: log}) do
-    render_many(log, __MODULE__, "show.json", as: :log_entry)
+    %{
+      items: render_many(log.items, __MODULE__, "show.json", as: :log_entry),
+      total: log.count
+    }
   end
 
   def render("show.json", %{log_entry: log_entry}) do
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
new file mode 100644 (file)
index 0000000..aa7c8c3
--- /dev/null
@@ -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()
+    |> full_payload()
+    |> expires_at()
+    |> poll()
+    |> with_valid(&in_reply_to/1)
+    |> with_valid(&attachments/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
index 4a74dc16f128cd459cf04efdf63211666f073d24..a00e4b0d8c32540dd390e37d94c9a9a7ef6bac59 100644 (file)
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.Activity
   alias Pleroma.ActivityExpiration
   alias Pleroma.Conversation.Participation
-  alias Pleroma.Emoji
   alias Pleroma.Object
   alias Pleroma.ThreadMute
   alias Pleroma.User
@@ -18,14 +17,11 @@ defmodule Pleroma.Web.CommonAPI do
   import Pleroma.Web.CommonAPI.Utils
 
   def follow(follower, followed) do
+    timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
+
     with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
          {:ok, activity} <- ActivityPub.follow(follower, followed),
-         {:ok, follower, followed} <-
-           User.wait_and_refresh(
-             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
-             follower,
-             followed
-           ) do
+         {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
       {:ok, follower, followed, activity}
     end
   end
@@ -76,8 +72,7 @@ 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}
index 52fbc162be6920156b867fe678137327670945ab..88a5f434a671277c0a949d22ad4173f0c07b5f37 100644 (file)
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.CommonAPI.Utils do
   import Pleroma.Web.Gettext
+  import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]
 
   alias Calendar.Strftime
   alias Pleroma.Activity
@@ -41,14 +42,6 @@ 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
index b53a01955d3cf0c1b60a7072385ec3a8b4c0b3f9..e90bf842ee37bf625dea3da85accc85f4a227865 100644 (file)
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do
   use Pleroma.Web, :controller
 
   # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
-  @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
+  @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
   def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
   def truthy_param?(value), do: value not in @falsy_param_values
 
index 1e88ff7feec2c1dba5a2772911f0d8881e17a2c6..e4ae632312a1da57d2312950d38d18c9b3afbbd0 100644 (file)
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
 
   import Pleroma.Web.ControllerHelper,
-    only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
+    only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
 
   alias Ecto.Changeset
   alias Pleroma.Activity
@@ -44,7 +44,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.TwitterAPI.TwitterAPI
 
-  alias Pleroma.Web.ControllerHelper
   import Ecto.Query
 
   require Logger
@@ -156,7 +155,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)
@@ -344,43 +343,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 =
@@ -400,25 +362,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
 
@@ -575,14 +518,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     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
+  def post_status(
+        %{assigns: %{user: user}} = conn,
+        %{"status" => _, "scheduled_at" => scheduled_at} = params
+      ) do
+    if ScheduledActivity.far_enough?(scheduled_at) do
       with {:ok, scheduled_activity} <-
              ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
         conn
@@ -590,24 +530,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         |> 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
+      post_status(conn, Map.drop(params, ["scheduled_at"]))
+    end
+  end
+
+  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
+    case CommonAPI.post(user, params) do
+      {:ok, activity} ->
+        conn
+        |> put_view(StatusView)
+        |> try_render("status.json", %{
+          activity: activity,
+          for: user,
+          as: :activity,
+          with_direct_conversation_id: true
+        })
+
+      {:error, message} ->
+        conn
+        |> put_status(:unprocessable_entity)
+        |> json(%{error: message})
     end
   end
 
@@ -822,45 +764,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     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
@@ -1173,31 +1076,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, res)
   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)
 
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 (file)
index 0000000..bb8b0eb
--- /dev/null
@@ -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
index 8d8e1ecdebb11c12e783c8acd5304f023092f5a7..5b744e898476037e625d630660c15e0e1ef2516e 100644 (file)
@@ -327,8 +327,8 @@ defmodule Pleroma.Web.Router do
       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)
@@ -474,9 +474,9 @@ 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)
index c7870847186f9c16a7970ffe7cab20a9728f709d..a39a00e0221ff2b3e8265540da28ab53980ec4a1 100644 (file)
@@ -30,8 +30,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
-               "@#{moderator.nickname} deleted user @#{subject1.nickname}"
+      assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
     end
 
     test "logging user creation by moderator", %{
@@ -48,7 +47,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"
     end
 
@@ -63,7 +62,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"
     end
 
@@ -78,7 +77,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"
     end
 
@@ -100,8 +99,7 @@ defmodule Pleroma.ModerationLogTest do
 
       tags = ["foo", "bar"] |> Enum.join(", ")
 
-      assert ModerationLog.get_log_entry_message(log) ==
-               "@#{admin.nickname} added tags: #{tags} to users: #{users}"
+      assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"
     end
 
     test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
@@ -122,7 +120,7 @@ defmodule Pleroma.ModerationLogTest do
 
       tags = ["foo", "bar"] |> Enum.join(", ")
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{admin.nickname} removed tags: #{tags} from users: #{users}"
     end
 
@@ -137,8 +135,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
-               "@#{moderator.nickname} made @#{subject1.nickname} moderator"
+      assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"
     end
 
     test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do
@@ -152,7 +149,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"
     end
 
@@ -166,7 +163,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} followed relay: https://example.org/relay"
     end
 
@@ -180,7 +177,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} unfollowed relay: https://example.org/relay"
     end
 
@@ -202,7 +199,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"
     end
 
@@ -224,7 +221,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"
     end
 
@@ -242,7 +239,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"
     end
 
@@ -260,7 +257,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"
     end
 
@@ -278,7 +275,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
+      assert log.data["message"] ==
                "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"
     end
 
@@ -294,8 +291,7 @@ defmodule Pleroma.ModerationLogTest do
 
       log = Repo.one(ModerationLog)
 
-      assert ModerationLog.get_log_entry_message(log) ==
-               "@#{moderator.nickname} deleted status ##{note.id}"
+      assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
     end
   end
 end
index 00e64692aa298664e6fe54d939956683433cd33b..b5c355e66f3eb9c36f6ad41bf676306f8f518c26 100644 (file)
@@ -2257,8 +2257,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   describe "GET /api/pleroma/admin/moderation_log" do
     setup %{conn: conn} do
       admin = insert(:user, info: %{is_admin: true})
+      moderator = insert(:user, info: %{is_moderator: true})
 
-      %{conn: assign(conn, :user, admin), admin: admin}
+      %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}
     end
 
     test "returns the log", %{conn: conn, admin: admin} do
@@ -2291,9 +2292,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       conn = get(conn, "/api/pleroma/admin/moderation_log")
 
       response = json_response(conn, 200)
-      [first_entry, second_entry] = response
+      [first_entry, second_entry] = response["items"]
 
-      assert response |> length() == 2
+      assert response["total"] == 2
       assert first_entry["data"]["action"] == "relay_unfollow"
 
       assert first_entry["message"] ==
@@ -2335,9 +2336,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
 
       response1 = json_response(conn1, 200)
-      [first_entry] = response1
+      [first_entry] = response1["items"]
 
-      assert response1 |> length() == 1
+      assert response1["total"] == 2
+      assert response1["items"] |> length() == 1
       assert first_entry["data"]["action"] == "relay_unfollow"
 
       assert first_entry["message"] ==
@@ -2346,14 +2348,119 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
 
       response2 = json_response(conn2, 200)
-      [second_entry] = response2
+      [second_entry] = response2["items"]
 
-      assert response2 |> length() == 1
+      assert response2["total"] == 2
+      assert response2["items"] |> length() == 1
       assert second_entry["data"]["action"] == "relay_follow"
 
       assert second_entry["message"] ==
                "@#{admin.nickname} followed relay: https://example.org/relay"
     end
+
+    test "filters log by date", %{conn: conn, admin: admin} do
+      first_date = "2017-08-15T15:47:06Z"
+      second_date = "2017-08-20T15:47:06Z"
+
+      Repo.insert(%ModerationLog{
+        data: %{
+          actor: %{
+            "id" => admin.id,
+            "nickname" => admin.nickname,
+            "type" => "user"
+          },
+          action: "relay_follow",
+          target: "https://example.org/relay"
+        },
+        inserted_at: NaiveDateTime.from_iso8601!(first_date)
+      })
+
+      Repo.insert(%ModerationLog{
+        data: %{
+          actor: %{
+            "id" => admin.id,
+            "nickname" => admin.nickname,
+            "type" => "user"
+          },
+          action: "relay_unfollow",
+          target: "https://example.org/relay"
+        },
+        inserted_at: NaiveDateTime.from_iso8601!(second_date)
+      })
+
+      conn1 =
+        get(
+          conn,
+          "/api/pleroma/admin/moderation_log?start_date=#{second_date}"
+        )
+
+      response1 = json_response(conn1, 200)
+      [first_entry] = response1["items"]
+
+      assert response1["total"] == 1
+      assert first_entry["data"]["action"] == "relay_unfollow"
+
+      assert first_entry["message"] ==
+               "@#{admin.nickname} unfollowed relay: https://example.org/relay"
+    end
+
+    test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do
+      Repo.insert(%ModerationLog{
+        data: %{
+          actor: %{
+            "id" => admin.id,
+            "nickname" => admin.nickname,
+            "type" => "user"
+          },
+          action: "relay_follow",
+          target: "https://example.org/relay"
+        }
+      })
+
+      Repo.insert(%ModerationLog{
+        data: %{
+          actor: %{
+            "id" => moderator.id,
+            "nickname" => moderator.nickname,
+            "type" => "user"
+          },
+          action: "relay_unfollow",
+          target: "https://example.org/relay"
+        }
+      })
+
+      conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}")
+
+      response1 = json_response(conn1, 200)
+      [first_entry] = response1["items"]
+
+      assert response1["total"] == 1
+      assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id
+    end
+
+    test "returns log filtered by search", %{conn: conn, moderator: moderator} do
+      ModerationLog.insert_log(%{
+        actor: moderator,
+        action: "relay_follow",
+        target: "https://example.org/relay"
+      })
+
+      ModerationLog.insert_log(%{
+        actor: moderator,
+        action: "relay_unfollow",
+        target: "https://example.org/relay"
+      })
+
+      conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo")
+
+      response1 = json_response(conn1, 200)
+      [first_entry] = response1["items"]
+
+      assert response1["total"] == 1
+
+      assert get_in(first_entry, ["data", "message"]) ==
+               "@#{moderator.nickname} unfollowed relay: https://example.org/relay"
+    end
   end
 
   describe "PATCH /users/:nickname/force_password_reset" do
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 (file)
index 0000000..d3652d9
--- /dev/null
@@ -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
index cd672132bb6a0542488f83dab8fd50877427ddf6..7f7a89516d2d0b1e94cf4fe78876cb3868a41553 100644 (file)
@@ -20,12 +20,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   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,82 +37,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)
@@ -419,80 +343,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     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 +406,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)
 
@@ -955,50 +778,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     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)
@@ -1554,62 +1333,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     assert url =~ "an_image"
   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)