Page MenuHomePhorge

No OneTemporary

Size
314 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index cd8123c5d..4a448c1a5 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -1,935 +1,943 @@
# Admin API
Authentication is required and the user must be an admin.
Configuration options:
* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
Note that client app needs to explicitly support admin scopes and request them when obtaining auth token.
## `GET /api/pleroma/admin/users`
### List users
- Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters:
- `local`: only local users
- `external`: only external users
- `active`: only active users
- `deactivated`: only deactivated users
- `is_admin`: users with admin role
- `is_moderator`: users with moderator role
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`)
- *optional* `tags`: **[string]** tags list
- *optional* `name`: **string** user display name
- *optional* `email`: **string** user email
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- Response:
```json
{
"page_size": integer,
"count": integer,
"users": [
{
"deactivated": bool,
"id": integer,
"nickname": string,
"roles": {
"admin": bool,
"moderator": bool
},
"local": bool,
"tags": array,
"avatar": string,
"display_name": string
},
...
]
}
```
## DEPRECATED `DELETE /api/pleroma/admin/users`
### Remove a user
- Params:
- `nickname`
- Response: User’s nickname
## `DELETE /api/pleroma/admin/users`
### Remove a user
- Params:
- `nicknames`
- Response: Array of user nicknames
### Create a user
- Method: `POST`
- Params:
`users`: [
{
`nickname`,
`email`,
`password`
}
]
- Response: User’s nickname
## `POST /api/pleroma/admin/users/follow`
### Make a user follow another user
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `POST /api/pleroma/admin/users/unfollow`
### Make a user unfollow another user
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `PATCH /api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
- Params:
- `nickname`
- Response: User’s object
```json
{
"deactivated": bool,
"id": integer,
"nickname": string
}
```
## `PUT /api/pleroma/admin/users/tag`
### Tag a list of users
- Params:
- `nicknames` (array)
- `tags` (array)
## `DELETE /api/pleroma/admin/users/tag`
### Untag a list of users
- Params:
- `nicknames` (array)
- `tags` (array)
## `GET /api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
## `GET /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist.
### Get user user permission groups membership per permission group
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Add user to permission group
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
## `POST /api/pleroma/admin/users/permission_group/:permission_group`
### Add users to permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
## `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
- Note: An admin cannot revoke their own admin status.
## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
### Remove users from permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the user
- Note: An admin cannot revoke their own admin status.
## `PATCH /api/pleroma/admin/users/activate`
### Activate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `PATCH /api/pleroma/admin/users/deactivate`
### Deactivate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user
- Params:
- `nickname` or `id`
- Response:
- On failure: `Not found`
- On success: JSON of the user
## `GET /api/pleroma/admin/users/:nickname_or_id/statuses`
### Retrive user's latest statuses
- Params:
- `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` – allows to see private statuses
- Response:
- On failure: `Not found`
- On success: JSON array of user's latest statuses
## `POST /api/pleroma/admin/relay`
### Follow a Relay
- Params:
- `relay_url`
- Response:
- On success: URL of the followed relay
## `DELETE /api/pleroma/admin/relay`
### Unfollow a Relay
- Params:
- `relay_url`
- Response:
- On success: URL of the unfollowed relay
## `GET /api/pleroma/admin/relay`
### List Relays
- Params: none
- Response:
- On success: JSON array of relays
## `POST /api/pleroma/admin/users/invite_token`
### Create an account registration invite token
- Params:
- *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07")
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `GET /api/pleroma/admin/users/invites`
### Get a list of generated invites
- Params: none
- Response:
```json
{
"invites": [
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
},
...
]
}
```
## `POST /api/pleroma/admin/users/revoke_invite`
### Revoke invite by token
- Params:
- `token`
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `POST /api/pleroma/admin/users/email_invite`
### Sends registration invite via email
- Params:
- `email`
- `name`, optional
## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname
- Params: none
- Response:
```json
{
"token": "base64 reset token",
"link": "https://pleroma.social/api/pleroma/password_reset/url-encoded-base64-token"
}
```
## `PATCH /api/pleroma/admin/users/force_password_reset`
### Force passord reset for a user with a given nickname
- Params:
- `nicknames`
- Response: none (code `204`)
## `GET /api/pleroma/admin/reports`
### Get a list of reports
- Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
- `account`: the user who has been reported
- `actor`: the user who has sent the report
- `statuses`: list of statuses that have been included to the report
```json
{
"totalReports" : 1,
"reports": [
{
"account": {
"acct": "user",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-04-23T17:32:04.000Z",
"display_name": "User",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9i6dAJqSGSKMzLG2Lo",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": true,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 3,
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
"actor": {
"acct": "lain",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-03-28T17:36:03.000Z",
"display_name": "Roger Braun",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9hEkA5JsvAdlSrocam",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": false,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 1,
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
"state": "open",
"statuses": [
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `GET /api/pleroma/admin/grouped_reports`
### Get a list of reports, grouped by status
- Params: none
- On success: JSON, returns a list of reports, where:
- `date`: date of the latest report
- `account`: the user who has been reported (see `/api/pleroma/admin/reports` for reference)
- `status`: reported status (see `/api/pleroma/admin/reports` for reference)
- `actors`: users who had reported this status (see `/api/pleroma/admin/reports` for reference)
- `reports`: reports (see `/api/pleroma/admin/reports` for reference)
```json
"reports": [
{
"date": "2019-10-07T12:31:39.615149Z",
"account": { ... },
"status": { ... },
"actors": [{ ... }, { ... }],
"reports": [{ ... }]
}
]
```
## `GET /api/pleroma/admin/reports/:id`
### Get an individual report
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `PATCH /api/pleroma/admin/reports`
### Change the state of one or multiple reports
- Params:
```json
`reports`: [
{
`id`, // required, report id
`state` // required, the new state. Valid values are `open`, `closed` and `resolved`
},
...
]
```
- Response:
- On failure:
- 400 Bad Request, JSON:
```json
[
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/notes`
### Create report note
- Params:
- `id`: required, report id
- `content`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
### Delete report note
- Params:
- `report_id`: required, report id
- `id`: required, note id
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `DELETE /api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`
## `GET /api/pleroma/admin/restart`
### Restarts pleroma application
- Params: none
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{}
```
## `GET /api/pleroma/admin/config`
### Get list of merged default settings with saved in database.
+*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.*
+
**Only works when configuration from database is enabled.**
- Params:
- `only_db`: true (*optional*, get only saved in database settings)
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{
- configs: [
+ "configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": []
}
- ]
+ ],
+ "need_reboot": true
}
```
+ need_reboot - *optional*, if were changed reboot time settings.
## `POST /api/pleroma/admin/config`
### Update config settings
+*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.*
+
**Only works when configuration from database is enabled.**
Some modifications are necessary to save the config settings correctly:
- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules;
```
"Pleroma.Upload" -> Pleroma.Upload
"Oban" -> Oban
```
- strings starting with `:` will be converted to atoms;
```
":pleroma" -> :pleroma
```
- objects with `tuple` key and array value will be converted to tuples;
```
{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []}
```
- arrays with *tuple objects* will be converted to keywords;
```
[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"]
```
Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
- all settings inside these keys:
- `:hackney_pools`
- `:connections_pool`
- `:pools`
- `:chat`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
- Params:
- `configs` - array of config objects
- config object params:
- `group` - string (**required**)
- `key` - string (**required**)
- `value` - string, [], {} or {"tuple": []} (**required**)
- `delete` - true (*optional*, if setting must be deleted)
- `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored)
*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.*
```
[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value
{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion
[subkey2: val2] \\ value after deletion
```
*Most of the settings can be partially updated through merge old values with new values, except settings value of which is list or is not keyword.*
Example of setting without keyword in value:
```elixir
config :tesla, :adapter, Tesla.Adapter.Hackney
```
List of settings which support only full update by key:
```elixir
@full_key_update [
{:pleroma, :ecto_repos},
{:quack, :meta},
{:mime, :types},
{:cors_plug, [:max_age, :methods, :expose, :headers]},
{:auto_linker, :opts},
{:swarm, :node_blacklist},
{:logger, :backends}
]
```
List of settings which support only full update by subkey:
```elixir
@full_subkey_update [
{:pleroma, :assets, :mascots},
{:pleroma, :emoji, :groups},
{:pleroma, :workers, :retries},
{:pleroma, :mrf_subchain, :match_actor},
{:pleroma, :mrf_keyword, :replace}
]
```
*Settings without explicit key must be sended in separate config object params.*
```elixir
config :quack,
level: :debug,
meta: [:all],
...
```
```json
{
- configs: [
+ "configs": [
{"group": ":quack", "key": ":level", "value": ":debug"},
{"group": ":quack", "key": ":meta", "value": [":all"]},
...
]
}
```
- Request:
```json
{
- configs: [
+ "configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [
{"tuple": [":uploader", "Pleroma.Uploaders.Local"]},
{"tuple": [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
{"tuple": [":link_name", true]},
{"tuple": [":proxy_remote", false]},
{"tuple": [":proxy_opts", [
{"tuple": [":redirect_on_failure", false]},
{"tuple": [":max_body_length", 1048576]},
{"tuple": [":http", [
{"tuple": [":follow_redirect", true]},
{"tuple": [":pool", ":upload"]},
]]}
]
]},
{"tuple": [":dispatch", {
"tuple": ["/api/v1/streaming", "Pleroma.Web.MastodonAPI.WebsocketHandler", []]
}]}
]
}
]
}
```
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{
- configs: [
+ "configs": [
{
"group": ":pleroma",
"key": "Pleroma.Upload",
"value": [...]
}
- ]
+ ],
+ "need_reboot": true
}
```
+need_reboot - *optional*, if were changed reboot time settings.
## ` GET /api/pleroma/admin/config/descriptions`
### Get JSON with config descriptions.
Loads json generated from `config/descriptions.exs`.
- Params: none
- Response:
```json
[{
"group": ":pleroma", // string
"key": "ModuleName", // string
"type": "group", // string or list with possible values,
"description": "Upload general settings", // string
"children": [
{
"key": ":uploader", // string or module name `Pleroma.Upload`
"type": "module",
"description": "Module which will be used for uploads",
"suggestions": ["module1", "module2"]
},
{
"key": ":filters",
"type": ["list", "module"],
"description": "List of filter modules for uploads",
"suggestions": [
"module1", "module2", "module3"
]
}
]
}]
```
## `GET /api/pleroma/admin/moderation_log`
### Get moderation log
- Params:
- *optional* `page`: **integer** page number
- *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
[
{
"data": {
"actor": {
"id": 1,
"nickname": "lain"
},
"action": "relay_follow"
},
"time": 1502812026, // timestamp
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
}
]
```
## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji
- Authentication: required
- Params: None
- Response: JSON, "ok" and 200 status
## `PATCH /api/pleroma/admin/users/confirm_email`
### Confirm users' emails
- Params:
- `nicknames`
- Response: Array of user nicknames
## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
### Resend confirmation email
- Params:
- `nicknames`
- Response: Array of user nicknames
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 251074aaa..0087ab0e4 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -1,184 +1,182 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTask do
use Task
alias Pleroma.ConfigDB
alias Pleroma.Repo
require Logger
@type env() :: :test | :benchmark | :dev | :prod
@reboot_time_keys [
{:pleroma, :hackney_pools},
{:pleroma, :chat},
{:pleroma, Oban},
{:pleroma, :rate_limit},
{:pleroma, :markup},
{:pleroma, :streamer},
{:pleroma, :pools},
{:pleroma, :connections_pool},
{:tesla, :adapter}
]
@reboot_time_subkeys [
{:pleroma, Pleroma.Captcha, [:seconds_valid]},
{:pleroma, Pleroma.Upload, [:proxy_remote]},
{:pleroma, :instance, [:upload_limit]},
{:pleroma, :email_notifications, [:digest]},
{:pleroma, :oauth2, [:clean_expired_tokens]},
{:pleroma, Pleroma.ActivityExpiration, [:enabled]},
{:pleroma, Pleroma.ScheduledActivity, [:enabled]},
{:pleroma, :gopher, [:enabled]}
]
@reject [nil, :prometheus]
def start_link(_) do
load_and_update_env()
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore
end
@spec load_and_update_env([ConfigDB.t()]) :: :ok | false
def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do
with true <- Pleroma.Config.get(:configurable_from_database),
true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"),
started_applications <- Application.started_applications() do
# We need to restart applications for loaded settings take effect
in_db = Repo.all(ConfigDB)
with_deleted = in_db ++ deleted
reject_for_restart = if restart_pleroma?, do: @reject, else: [:pleroma | @reject]
applications =
with_deleted
|> Enum.map(&merge_and_update(&1))
|> Enum.uniq()
# TODO: some problem with prometheus after restart!
|> Enum.reject(&(&1 in reject_for_restart))
# to be ensured that pleroma will be restarted last
applications =
if :pleroma in applications do
List.delete(applications, :pleroma) ++ [:pleroma]
else
applications
end
Enum.each(applications, &restart(started_applications, &1, Pleroma.Config.get(:env)))
:ok
end
end
defp group_for_restart(:logger, key, _, merged_value) do
# change logger configuration in runtime, without restart
if Keyword.keyword?(merged_value) and
key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
Logger.configure_backend(key, merged_value)
else
Logger.configure([{key, merged_value}])
end
nil
end
defp group_for_restart(:tesla, _, _, _), do: :pleroma
defp group_for_restart(group, _, _, _) when group != :pleroma, do: group
defp group_for_restart(group, key, value, _) do
if pleroma_need_restart?(group, key, value) do
group
end
end
defp merge_and_update(setting) do
try do
key = ConfigDB.from_string(setting.key)
group = ConfigDB.from_string(setting.group)
default = Pleroma.Config.Holder.config(group, key)
value = ConfigDB.from_binary(setting.value)
merged_value =
if Ecto.get_meta(setting, :state) == :deleted do
default
else
if can_be_merged?(default, value) do
ConfigDB.merge_group(group, key, default, value)
else
value
end
end
:ok = update_env(group, key, merged_value)
group_for_restart(group, key, value, merged_value)
rescue
error ->
error_msg =
"updating env causes error, group: " <>
inspect(setting.group) <>
" key: " <>
inspect(setting.key) <>
" value: " <>
inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
Logger.warn(error_msg)
nil
end
end
@spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
def pleroma_need_restart?(group, key, value) do
group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
end
defp group_and_key_need_reboot?(group, key) do
Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end)
end
defp group_and_subkey_need_reboot?(group, key, value) do
Keyword.keyword?(value) and
Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} ->
g == group and k == key and
Enum.any?(Keyword.keys(value), &(&1 in subkeys))
end)
end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
- defp restart(_, :pleroma, :test), do: Logger.warn("pleroma restarted")
-
- defp restart(_, :pleroma, _), do: send(Restarter.Pleroma, :after_boot)
+ defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
defp restart(started_applications, app, _) do
with {^app, _, _} <- List.keyfind(started_applications, app, 0),
:ok <- Application.stop(app) do
:ok = Application.start(app)
else
nil ->
Logger.warn("#{app} is not started.")
error ->
error
|> inspect()
|> Logger.warn()
end
end
defp can_be_merged?(val1, val2) when is_list(val1) and is_list(val2) do
Keyword.keyword?(val1) and Keyword.keyword?(val2)
end
defp can_be_merged?(_val1, _val2), do: false
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 11513106e..05946aa96 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -1,118 +1,119 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers
def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
dir
|> Pleroma.Utils.compile_dir()
|> case do
{:error, _errors, _warnings} ->
raise "Compiling scrubbers failed"
{:ok, _modules, _warnings} ->
:ok
end
end
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers do
Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers
end
def filter_tags(html, nil) do
filter_tags(html, get_scrubbers())
end
def filter_tags(html, scrubbers) when is_list(scrubbers) do
Enum.reduce(scrubbers, html, fn scrubber, html ->
filter_tags(html, scrubber)
end)
end
def filter_tags(html, scrubber) do
{:ok, content} = FastSanitize.Sanitizer.scrub(html, scrubber)
content
end
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags)
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
def get_cached_stripped_html_for_activity(content, activity, key) do
get_cached_scrubbed_html_for_activity(
content,
FastSanitize.Sanitizer.StripTags,
activity,
key,
&HtmlEntities.decode/1
)
end
def ensure_scrubbed_html(
content,
scrubbers,
fake,
callback
) do
content =
content
|> filter_tags(scrubbers)
|> callback.()
if fake do
{:ignore, content}
else
{:commit, content}
end
end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
defp generate_scrubber_signature(scrubbers) do
Enum.reduce(scrubbers, "", fn scrubber, signature ->
"#{signature}#{to_string(scrubber)}"
end)
end
def extract_first_external_url(_, nil), do: {:error, "No content"}
def extract_first_external_url(object, content) do
key = "URL|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
result =
content
+ |> Floki.parse_fragment!()
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href")
|> Enum.at(0)
{:commit, {:ok, result}}
end)
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
index 8abe18e29..802d10edc 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
@@ -1,54 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
require Logger
# has the user successfully posted before?
defp old_user?(%User{} = u) do
u.note_count > 0 || u.follower_count > 0
end
# does the post contain links?
defp contains_links?(%{"content" => content} = _object) do
content
+ |> Floki.parse_fragment!()
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl")
|> Floki.attribute("a", "href")
|> length() > 0
end
defp contains_links?(_), do: false
@impl true
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message}
else
{:contains_links, false} ->
{:ok, message}
{:old_user, false} ->
{:reject, nil}
{:error, _} ->
{:reject, nil}
e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
{:reject, nil}
end
end
# in all other cases, pass through
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index 8b36c1021..788508349 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -1,101 +1,100 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
alias Pleroma.Config
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.MRF
require Pleroma.Constants
@moduledoc "Filter activities depending on their age"
- @behaviour MRF
+ @behaviour Pleroma.Web.ActivityPub.MRF
defp check_date(%{"published" => published} = message) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
{:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
{:ok, message}
else
{:ttl, true} ->
{:reject, nil}
e ->
{:error, e}
end
end
defp check_reject(message, actions) do
if :reject in actions do
{:reject, nil}
else
{:ok, message}
end
end
defp check_delist(message, actions) do
if :delist in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
to = List.delete(message["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(message["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
end
else
{:ok, message}
end
end
defp check_strip_followers(message, actions) do
if :strip_followers in actions do
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
to = List.delete(message["to"], user.follower_address)
cc = List.delete(message["cc"], user.follower_address)
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
end
else
{:ok, message}
end
end
@impl true
def filter(%{"type" => "Create", "published" => _} = message) do
with actions <- Config.get([:mrf_object_age, :actions]),
{:reject, _} <- check_date(message),
{:ok, message} <- check_reject(message, actions),
{:ok, message} <- check_delist(message, actions),
{:ok, message} <- check_strip_followers(message, actions) do
{:ok, message}
else
# check_date() is allowed to short-circuit the pipeline
e -> e
end
end
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 8e53296e7..b94c3c78a 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -1,194 +1,194 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
@moduledoc "Filter activities depending on their origin instance"
- @behaviour MRF
+ @behaviour Pleroma.Web.ActivityPub.MRF
require Pleroma.Constants
defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts =
Pleroma.Config.get([:mrf_simple, :accept])
|> MRF.subdomains_regex()
cond do
accepts == [] -> {:ok, object}
actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil}
end
end
defp check_reject(%{host: actor_host} = _actor_info, object) do
rejects =
Pleroma.Config.get([:mrf_simple, :reject])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do
{:reject, nil}
else
{:ok, object}
end
end
defp check_media_removal(
%{host: actor_host} = _actor_info,
%{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
)
when length(child_attachment) > 0 do
media_removal =
Pleroma.Config.get([:mrf_simple, :media_removal])
|> MRF.subdomains_regex()
object =
if MRF.subdomain_match?(media_removal, actor_host) do
child_object = Map.delete(object["object"], "attachment")
Map.put(object, "object", child_object)
else
object
end
{:ok, object}
end
defp check_media_removal(_actor_info, object), do: {:ok, object}
defp check_media_nsfw(
%{host: actor_host} = _actor_info,
%{
"type" => "Create",
"object" => child_object
} = object
) do
media_nsfw =
Pleroma.Config.get([:mrf_simple, :media_nsfw])
|> MRF.subdomains_regex()
object =
if MRF.subdomain_match?(media_nsfw, actor_host) do
tags = (child_object["tag"] || []) ++ ["nsfw"]
child_object = Map.put(child_object, "tag", tags)
child_object = Map.put(child_object, "sensitive", true)
Map.put(object, "object", child_object)
else
object
end
{:ok, object}
end
defp check_media_nsfw(_actor_info, object), do: {:ok, object}
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
timeline_removal =
Pleroma.Config.get([:mrf_simple, :federated_timeline_removal])
|> MRF.subdomains_regex()
object =
with true <- MRF.subdomain_match?(timeline_removal, actor_host),
user <- User.get_cached_by_ap_id(object["actor"]),
true <- Pleroma.Constants.as_public() in object["to"] do
to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
object
|> Map.put("to", to)
|> Map.put("cc", cc)
else
_ -> object
end
{:ok, object}
end
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
report_removal =
Pleroma.Config.get([:mrf_simple, :report_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do
{:reject, nil}
else
{:ok, object}
end
end
defp check_report_removal(_actor_info, object), do: {:ok, object}
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
avatar_removal =
Pleroma.Config.get([:mrf_simple, :avatar_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(avatar_removal, actor_host) do
{:ok, Map.delete(object, "icon")}
else
{:ok, object}
end
end
defp check_avatar_removal(_actor_info, object), do: {:ok, object}
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
banner_removal =
Pleroma.Config.get([:mrf_simple, :banner_removal])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(banner_removal, actor_host) do
{:ok, Map.delete(object, "image")}
else
{:ok, object}
end
end
defp check_banner_removal(_actor_info, object), do: {:ok, object}
@impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)
with {:ok, object} <- check_accept(actor_info, object),
{:ok, object} <- check_reject(actor_info, object),
{:ok, object} <- check_media_removal(actor_info, object),
{:ok, object} <- check_media_nsfw(actor_info, object),
{:ok, object} <- check_ftl_removal(actor_info, object),
{:ok, object} <- check_report_removal(actor_info, object) do
{:ok, object}
else
_e -> {:reject, nil}
end
end
def filter(%{"id" => actor, "type" => obj_type} = object)
when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
actor_info = URI.parse(actor)
with {:ok, object} <- check_accept(actor_info, object),
{:ok, object} <- check_reject(actor_info, object),
{:ok, object} <- check_avatar_removal(actor_info, object),
{:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object}
else
_e -> {:reject, nil}
end
end
def filter(object), do: {:ok, object}
@impl true
def describe do
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
mrf_simple =
Pleroma.Config.get(:mrf_simple)
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|> Enum.into(%{})
{:ok, %{mrf_simple: mrf_simple}}
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
index 566c1e191..77ffd1bb9 100644
--- a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
@@ -1,43 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
- @behaviour MRF
+ @behaviour Pleroma.Web.ActivityPub.MRF
defp lookup_subchain(actor) do
with matches <- Config.get([:mrf_subchain, :match_actor]),
{match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do
{:ok, match, subchain}
else
_e -> {:error, :notfound}
end
end
@impl true
def filter(%{"actor" => actor} = message) do
with {:ok, match, subchain} <- lookup_subchain(actor) do
Logger.debug(
"[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{
inspect(subchain)
}"
)
subchain
|> MRF.filter(message)
else
_e -> {:ok, message}
end
end
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index c95cd182d..67222ebae 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -1,989 +1,1001 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
require Logger
@descriptions_json Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"], admin: true}
when action in [
:get_password_reset,
:user_delete,
:users_create,
:user_toggle_activation,
:user_activate,
:user_deactivate,
:tag_users,
:untag_users,
:right_add,
:right_delete
]
)
plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
plug(
OAuthScopesPlug,
%{scopes: ["write:invites"], admin: true}
when action in [:create_invite_token, :revoke_invite, :email_invite]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:reports"], admin: true}
when action in [:list_reports, :report_show]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:reports"], admin: true}
when action in [:reports_update]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
when action == :list_user_statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"], admin: true}
when action in [:status_update, :status_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read"], admin: true}
when action in [:config_show, :list_log]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"], admin: true}
when action == :config_update
)
action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "delete"
})
conn
|> json(nickname)
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.delete(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "delete"
})
conn
|> json(nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end
conn
|> json("ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end
conn
|> json("ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
changesets =
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
case Pleroma.Repo.transaction(changesets) do
{:ok, users} ->
res =
users
|> Map.values()
|> Enum.map(fn user ->
{:ok, user} = User.post_register_action(user)
user
end)
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
ModerationLog.insert_log(%{
actor: admin,
subjects: Map.values(users),
action: "create"
})
conn
|> json(res)
{:error, id, changeset, _} ->
res =
Enum.map(changesets.operations, fn
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
AccountView.render("create-error.json", %{changeset: changeset})
{_, {:changeset, current_changeset, _}} ->
AccountView.render("create-error.json", %{changeset: current_changeset})
end)
conn
|> put_status(:conflict)
|> json(res)
end
end
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_instance_activities(%{
"instance" => instance,
"limit" => page_size,
"offset" => (page - 1) * page_size
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
{_, page_size} = page_params(params)
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
"godmode" => godmode
})
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
end
end
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.deactivated)
action = if user.deactivated, do: "activate", else: "deactivate"
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: action
})
conn
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, false)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "activate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, true)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "deactivate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "tag"
})
json_response(conn, :no_content, "")
end
end
def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.untag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "untag"
})
json_response(conn, :no_content, "")
end
end
def list_users(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
search_params = %{
query: params["query"],
page: page,
page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"]
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
{:ok, users, count} <- filter_service_users(users, count),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
defp filter_service_users(users, count) do
filtered_users = Enum.reject(users, &service_user?/1)
count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
{:ok, filtered_users, count}
end
defp service_user?(user) do
String.match?(user.ap_id, ~r/.*\/relay$/) or
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
end
@filters ~w(local external active deactivated is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Enum.map(&String.to_atom(&1))
|> Enum.into(%{}, &{&1, true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nicknames" => nicknames
})
when permission_group in ["moderator", "admin"] do
update = %{:"is_#{permission_group}" => true}
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
for u <- users, do: User.admin_api_update(u, update)
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, update)
end
def right_add_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_add(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nickname" => nickname
})
when permission_group in ["moderator", "admin"] do
fields = %{:"is_#{permission_group}" => true}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: [user],
permission: permission_group
})
json(conn, fields)
end
def right_add(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_get(conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
conn
|> json(%{
is_moderator: user.is_moderator,
is_admin: user.is_admin
})
end
def right_delete_multiple(
%{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
%{
"permission_group" => permission_group,
"nicknames" => nicknames
}
)
when permission_group in ["moderator", "admin"] do
with false <- Enum.member?(nicknames, admin_nickname) do
update = %{:"is_#{permission_group}" => false}
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
for u <- users, do: User.admin_api_update(u, update)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, update)
else
_ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
end
end
def right_delete_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_delete(
%{assigns: %{user: admin}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
fields = %{:"is_#{permission_group}" => false}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: [user],
permission: permission_group
})
json(conn, fields)
end
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
def relay_list(conn, _params) do
with {:ok, list} <- Relay.list() do
json(conn, %{relays: list})
else
_ ->
conn
|> put_status(500)
end
end
def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{
action: "relay_follow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.unfollow(target) do
ModerationLog.insert_log(%{
action: "relay_unfollow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with true <-
- Pleroma.Config.get([:instance, :invites_enabled]) &&
- !Pleroma.Config.get([:instance, :registrations_open]),
+ Config.get([:instance, :invites_enabled]) &&
+ !Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
end
end
@doc "Create an account registration invite token"
def create_invite_token(conn, params) do
opts = %{}
opts =
if params["max_use"],
do: Map.put(opts, :max_use, params["max_use"]),
else: opts
opts =
if params["expires_at"],
do: Map.put(opts, :expires_at, params["expires_at"]),
else: opts
{:ok, invite} = UserInviteToken.create_invite(opts)
json(conn, AccountView.render("invite.json", %{invite: invite}))
end
@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()
conn
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end
@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end
@doc "Force password reset for a given user"
def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
Enum.each(users, &User.force_password_reset_async/1)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "force_password_reset"
})
json_response(conn, :no_content, "")
end
def list_reports(conn, params) do
{page, page_size} = page_params(params)
reports = Utils.get_reports(params, page, page_size)
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def list_grouped_reports(conn, _params) do
statuses = Utils.get_reported_activities()
conn
|> put_view(ReportView)
|> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
end
def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
result =
reports
|> Enum.map(fn report ->
with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: activity
})
activity
else
{:error, message} -> %{id: report["id"], error: message}
end
end)
case Enum.any?(result, &Map.has_key?(&1, :error)) do
true -> json_response(conn, :bad_request, result)
false -> json_response(conn, :no_content, "")
end
end
def report_notes_create(%{assigns: %{user: user}} = conn, %{
"id" => report_id,
"content" => content
}) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
ModerationLog.insert_log(%{
action: "report_note",
actor: user,
subject: Activity.get_by_id(report_id),
text: content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def report_notes_delete(%{assigns: %{user: user}} = conn, %{
"id" => note_id,
"report_id" => report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
text: note.content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
visibility: params["visibility"]
})
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
ModerationLog.insert_log(%{
action: "status_delete",
actor: user,
subject_id: id
})
json(conn, %{})
end
end
def list_log(conn, params) do
{page, page_size} = page_params(params)
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)
|> render("index.json", %{log: log})
end
def config_descriptions(conn, _params) do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, @descriptions_json)
end
def config_show(conn, %{"only_db" => true}) do
with :ok <- configurable_from_database(conn) do
configs = Pleroma.Repo.all(ConfigDB)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
end
def config_show(conn, _params) do
with :ok <- configurable_from_database(conn) do
configs = ConfigDB.get_all_as_keyword()
merged =
- Pleroma.Config.Holder.config()
+ Config.Holder.config()
|> ConfigDB.merge(configs)
|> Enum.map(fn {group, values} ->
Enum.map(values, fn {key, value} ->
db =
if configs[group][key] do
ConfigDB.get_db_keys(configs[group][key], key)
end
db_value = configs[group][key]
merged_value =
if !is_nil(db_value) and Keyword.keyword?(db_value) and
ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
ConfigDB.merge_group(group, key, value, db_value)
else
value
end
setting = %{
group: ConfigDB.convert(group),
key: ConfigDB.convert(key),
value: ConfigDB.convert(merged_value)
}
if db, do: Map.put(setting, :db, db), else: setting
end)
end)
|> List.flatten()
- json(conn, %{configs: merged})
+ response = %{configs: merged}
+
+ response =
+ if Restarter.Pleroma.need_reboot?() do
+ Map.put(response, :need_reboot, true)
+ else
+ response
+ end
+
+ json(conn, response)
end
end
def config_update(conn, %{"configs" => configs}) do
with :ok <- configurable_from_database(conn) do
{_errors, results} =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
%{"group" => group, "key" => key, "value" => value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.split_with(fn result -> elem(result, 0) == :error end)
{deleted, updated} =
results
|> Enum.map(fn {:ok, config} ->
Map.put(config, :db, ConfigDB.get_db_keys(config))
end)
|> Enum.split_with(fn config ->
Ecto.get_meta(config, :state) == :deleted
end)
- Pleroma.Config.TransferTask.load_and_update_env(deleted, false)
+ Config.TransferTask.load_and_update_env(deleted, false)
need_reboot? =
- Enum.any?(updated, fn config ->
- group = ConfigDB.from_string(config.group)
- key = ConfigDB.from_string(config.key)
- value = ConfigDB.from_binary(config.value)
- Pleroma.Config.TransferTask.pleroma_need_restart?(group, key, value)
- end)
+ Restarter.Pleroma.need_reboot?() ||
+ Enum.any?(updated, fn config ->
+ group = ConfigDB.from_string(config.group)
+ key = ConfigDB.from_string(config.key)
+ value = ConfigDB.from_binary(config.value)
+ Config.TransferTask.pleroma_need_restart?(group, key, value)
+ end)
response = %{configs: updated}
response =
- if need_reboot?, do: Map.put(response, :need_reboot, need_reboot?), else: response
+ if need_reboot? do
+ Restarter.Pleroma.need_reboot()
+ Map.put(response, :need_reboot, need_reboot?)
+ else
+ response
+ end
conn
|> put_view(ConfigView)
|> render("index.json", response)
end
end
def restart(conn, _params) do
with :ok <- configurable_from_database(conn) do
- if Pleroma.Config.get(:env) == :test do
- Logger.warn("pleroma restarted")
- else
- send(Restarter.Pleroma, {:restart, 50})
- end
+ Restarter.Pleroma.restart(Config.get(:env), 50)
json(conn, %{})
end
end
defp configurable_from_database(conn) do
- if Pleroma.Config.get(:configurable_from_database) do
+ if Config.get(:configurable_from_database) do
:ok
else
errors(
conn,
{:error, "To use this endpoint you need to enable configuration from database."}
)
end
end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
conn |> json("ok")
end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.toggle_confirmation(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "confirm_email"
})
conn |> json("")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.try_send_confirmation_email(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "resend_confirmation_email"
})
conn |> json("")
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, {:error, reason}) do
conn
|> put_status(:bad_request)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
def errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])}
end
defp get_page(page_string) when is_nil(page_string), do: 1
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index d4695c1c6..6b0fe9215 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,499 +1,501 @@
# 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.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
defp get_user(ap_id) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
true ->
User.error_user(ap_id)
end
end
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
do: Utils.context_to_conversation_id(context)
defp get_context_id(_), do: nil
defp reblogged?(activity, user) do
object = Object.normalize(activity) || %{}
present?(user && user.ap_id in (object.data["announcements"] || []))
end
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
opts = Map.put(opts, :replied_to_activities, replied_to_activities)
safe_render_many(opts.activities, StatusView, "show.json", opts)
end
def render(
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
reblogged_activity =
Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
mentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
%{
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
sensitive: false,
spoiler_text: "",
visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: [],
pleroma: %{
local: activity.local
}
}
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
tags = object.data["tag"] || []
sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
tag_mentions =
tags
|> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
|> Enum.map(fn tag -> tag["href"] end)
mentions =
(object.data["to"] ++ tag_mentions)
|> Enum.uniq()
|> Enum.map(fn
Pleroma.Constants.as_public() -> nil
^user_follower_address -> nil
ap_id -> User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
expires_at =
with true <- client_posted_this_activity,
- expiration when not is_nil(expiration) <-
+ %ActivityExpiration{scheduled_at: scheduled_at} <-
ActivityExpiration.get_by_activity_id(activity.id) do
- expiration.scheduled_at
+ scheduled_at
+ else
+ _ -> nil
end
thread_muted? =
case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
end
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["published"])
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && get_user(reply_to.data["actor"])
content =
object
|> render_content()
content_html =
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content"
)
content_plaintext =
content
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content"
)
summary = object.data["summary"] || ""
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
url =
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object.data["url"] || object.data["external_url"] || object.data["id"]
end
direct_conversation_id =
with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
{_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]} do
Activity.direct_conversation_id(activity, for_user)
else
{:direct_conversation_id, participation_id} when is_integer(participation_id) ->
participation_id
_e ->
nil
end
emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do
Enum.map(emoji_reactions, fn [emoji, users] ->
%{
name: emoji,
count: length(users),
me: !!(opts[:for] && opts[:for].ap_id in users)
}
end)
else
_ -> []
end
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content_html,
created_at: created_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: thread_muted? || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: summary,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
emoji_reactions: emoji_reactions
}
}
end
def render("show.json", _) do
nil
end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url_data = URI.parse(page_url)
page_url_data =
if rich_media[:url] != nil do
URI.merge(page_url_data, URI.parse(rich_media[:url]))
else
page_url_data
end
page_url = page_url_data |> to_string
image_url =
if rich_media[:image] != nil do
URI.merge(page_url_data, URI.parse(rich_media[:image]))
|> to_string
else
nil
end
%{
type: "link",
provider_name: page_url_data.host,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url |> MediaProxy.url(),
title: rich_media[:title] || "",
description: rich_media[:description] || "",
pleroma: %{
opengraph: rich_media
}
}
end
def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
type =
cond do
String.contains?(media_type, "image") -> "image"
String.contains?(media_type, "video") -> "video"
String.contains?(media_type, "audio") -> "audio"
true -> "unknown"
end
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
%{
id: to_string(attachment["id"] || hash_id),
url: href,
remote_url: href,
preview_url: href,
text_url: href,
type: type,
description: attachment["name"],
pleroma: %{mime_type: media_type}
}
end
def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
%{
id: activity.id,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
created_at: created_at,
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
length: object.data["length"]
}
end
def render("listens.json", opts) do
safe_render_many(opts.activities, StatusView, "listen.json", opts)
end
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])
%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
end
def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Video", "Event"] do
with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
_ -> object.data["content"] || ""
end
end
def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Article", "Page"] do
with summary when not is_nil(summary) and summary != "" <- object.data["name"],
url when is_bitstring(url) <- object.data["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
else
_ -> object.data["content"] || ""
end
end
def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
[{"name": "fediverse", "url": "/tag/fediverse"},
{"name": "nextcloud", "url": "/tag/nextcloud"}]
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
Enum.reduce(object_tags, [], fn tag, tags ->
tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
end)
end
def build_tags(_), do: []
@doc """
Builds list emojis.
Arguments: `nil` or list tuple of name and url.
Returns list emojis.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
"""
@spec build_emojis(nil | list(tuple())) :: list(map())
def build_emojis(nil), do: []
def build_emojis(emojis) do
emojis
|> Enum.map(fn {name, url} ->
name = HTML.strip_tags(name)
url =
url
|> HTML.strip_tags()
|> MediaProxy.url()
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
end)
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
end
diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex
index f87fc1973..86dcc1a3b 100644
--- a/lib/pleroma/web/metadata/rel_me.ex
+++ b/lib/pleroma/web/metadata/rel_me.ex
@@ -1,17 +1,19 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.RelMe do
alias Pleroma.Web.Metadata.Providers.Provider
@behaviour Provider
@impl Provider
def build_tags(%{user: user}) do
- (Floki.attribute(user.bio, "link[rel~=me]", "href") ++
- Floki.attribute(user.bio, "a[rel~=me]", "href"))
+ bio_tree = Floki.parse_fragment!(user.bio)
+
+ (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++
+ Floki.attribute(bio_tree, "a[rel~=me]", "href"))
|> Enum.map(fn link ->
{:link, [rel: "me", href: link], []}
end)
end
end
diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex
index 0ae926375..b33dd64dd 100644
--- a/lib/pleroma/web/rel_me.ex
+++ b/lib/pleroma/web/rel_me.ex
@@ -1,60 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RelMe do
@options [
pool: :media,
max_body: 2_000_000
]
if Pleroma.Config.get(:env) == :test do
def parse(url) when is_binary(url), do: parse_url(url)
else
def parse(url) when is_binary(url) do
Cachex.fetch!(:rel_me_cache, url, fn _ ->
{:commit, parse_url(url)}
end)
rescue
e -> {:error, "Cachex error: #{inspect(e)}"}
end
end
def parse(_), do: {:error, "No URL provided"}
defp parse_url(url) do
opts =
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
Keyword.merge(@options,
recv_timeout: 2_000,
with_body: true
)
else
@options
end
with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <-
Pleroma.HTTP.get(url, [], adapter: opts),
+ {:ok, html_tree} <- Floki.parse_document(html),
data <-
- Floki.attribute(html, "link[rel~=me]", "href") ++
- Floki.attribute(html, "a[rel~=me]", "href") do
+ Floki.attribute(html_tree, "link[rel~=me]", "href") ++
+ Floki.attribute(html_tree, "a[rel~=me]", "href") do
{:ok, data}
end
rescue
e -> {:error, "Parsing error: #{inspect(e)}"}
end
def maybe_put_rel_me("http" <> _ = target_page, profile_urls) when is_list(profile_urls) do
{:ok, rel_me_hrefs} = parse(target_page)
true = Enum.any?(rel_me_hrefs, fn x -> x in profile_urls end)
"me"
rescue
_ -> nil
end
def maybe_put_rel_me(_, _) do
nil
end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index 9deb03845..e05a0a204 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -1,134 +1,134 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do
@options [
pool: :media,
max_body: 2_000_000
]
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
end
def parse(nil), do: {:error, "No URL provided"}
if Pleroma.Config.get(:env) == :test do
def parse(url), do: parse_url(url)
else
def parse(url) do
try do
Cachex.fetch!(:rich_media_cache, url, fn _ ->
{:commit, parse_url(url)}
end)
|> set_ttl_based_on_image(url)
rescue
e ->
{:error, "Cachex error: #{inspect(e)}"}
end
end
end
@doc """
Set the rich media cache based on the expiration time of image.
Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
## Example
defmodule MyModule do
@behaviour Pleroma.Web.RichMedia.Parser.TTL
def ttl(data, url) do
image_url = Map.get(data, :image)
# do some parsing in the url and get the ttl of the image
# and return ttl is unix time
parse_ttl_from_url(image_url)
end
end
Define the module in the config
config :pleroma, :rich_media,
ttl_setters: [MyModule]
"""
def set_ttl_based_on_image({:ok, data}, url) do
with {:ok, nil} <- Cachex.ttl(:rich_media_cache, url),
ttl when is_number(ttl) <- get_ttl_from_image(data, url) do
Cachex.expire_at(:rich_media_cache, url, ttl * 1000)
{:ok, data}
else
_ ->
{:ok, data}
end
end
defp get_ttl_from_image(data, url) do
Pleroma.Config.get([:rich_media, :ttl_setters])
|> Enum.reduce({:ok, nil}, fn
module, {:ok, _ttl} ->
module.ttl(data, url)
_, error ->
error
end)
end
defp parse_url(url) do
opts =
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
Keyword.merge(@options,
recv_timeout: 2_000,
with_body: true
)
else
@options
end
try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts)
html
- |> parse_html
+ |> parse_html()
|> maybe_parse()
|> Map.put(:url, url)
|> clean_parsed_data()
|> check_parsed_data()
rescue
e ->
- {:error, "Parsing error: #{inspect(e)}"}
+ {:error, "Parsing error: #{inspect(e)} #{inspect(__STACKTRACE__)}"}
end
end
- defp parse_html(html), do: Floki.parse(html)
+ defp parse_html(html), do: Floki.parse_document!(html)
defp maybe_parse(html) do
Enum.reduce_while(parsers(), %{}, fn parser, acc ->
case parser.parse(html, acc) do
{:ok, data} -> {:halt, data}
{:error, _msg} -> {:cont, acc}
end
end)
end
defp check_parsed_data(%{title: title} = data)
when is_binary(title) and byte_size(title) > 0 do
{:ok, data}
end
defp check_parsed_data(data) do
{:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
end
defp clean_parsed_data(data) do
data
|> Enum.reject(fn {key, val} ->
with {:ok, _} <- Jason.encode(%{key => val}) do
false
else
_ -> true
end
end)
|> Map.new()
end
end
diff --git a/mix.exs b/mix.exs
index 7c6de5423..273307bbe 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,284 +1,284 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
version: version("1.1.50"),
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test],
# Docs
name: "Pleroma",
homepage_url: "https://pleroma.social/",
source_url: "https://git.pleroma.social/pleroma/pleroma",
docs: [
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
logo: "priv/static/static/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
Configuration: Path.wildcard("docs/config/*.md"),
Administration: Path.wildcard("docs/admin/*.md"),
"Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md")
],
main: "readme",
output: "priv/static/doc"
],
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load],
steps: [:assemble, &copy_files/1, &copy_nginx_config/1]
]
]
]
end
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
end
def copy_nginx_config(%{path: target_path} = release) do
File.cp!(
"./installation/pleroma.nginx",
Path.join([target_path, "installation", "pleroma.nginx"])
)
release
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Pleroma.Application, []},
extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize],
included_applications: [:ex_syslogger]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors(:prod), do: false
# Uncomment this if you need testing configurable_from_database logic
# defp warnings_as_errors(:dev), do: false
defp warnings_as_errors(_), do: true
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
dependency
else
[strategy] -> "ueberauth_#{strategy}"
end
end)
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.4.8"},
{:tzdata, "~> 0.5.21"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_enum, "~> 1.4"},
{:ecto_sql, "~> 3.3.2"},
{:postgrex, ">= 0.13.5"},
{:oban, "~> 0.12.1"},
{:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12.3"},
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.1"},
{:html_entities, "~> 0.5", override: true},
{:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"},
{:poison, "~> 3.0", override: true},
{:tesla, "~> 1.3", override: true},
{:castore, "~> 0.1"},
{:cowlib, "~> 2.8", override: true},
{:gun,
github: "ninenines/gun", ref: "bd6425ab87428cf4c95f4d23e0a48fd065fbd714", override: true},
{:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.1"},
{:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.6.6"},
{:earmark, "~> 1.3"},
{:bbcode, "~> 0.1.1"},
{:ex_machina, "~> 2.3", only: :test},
{:credo, "~> 1.1.0", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.3", only: :test},
{:crypt,
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:cors_plug, "~> 1.5"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
{:web_push_encryption, "~> 0.2.1"},
{:swoosh, "~> 0.23.2"},
{:phoenix_swoosh, "~> 0.2"},
{:gen_smtp, "~> 0.13"},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
- {:floki, "~> 0.23.0"},
{:ex_syslogger, "~> 1.4"},
+ {:floki, "~> 0.25"},
{:timex, "~> 3.5"},
{:ueberauth, "~> 0.4"},
{:auto_linker,
git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
{:http_signatures,
git: "https://git.pleroma.social/pleroma/http_signatures.git",
ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
{:prometheus_ex, "~> 3.0"},
{:prometheus_plugs, "~> 1.1"},
{:prometheus_phoenix, "~> 1.3"},
{:prometheus_ecto, "~> 1.4"},
{:recon, "~> 2.5"},
{:quack, "~> 0.1.1"},
{:joken, "~> 2.0"},
{:benchee, "~> 1.0"},
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.12.1", only: :test},
{:flake_id, "~> 0.1.0"},
{:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:mox, "~> 0.5", only: :test},
{:restarter, path: "./restarter"}
] ++ oauth_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.migrate": ["pleroma.ecto.migrate"],
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"],
docs: ["pleroma.docs", "docs"]
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * branch name
# * build metadata:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen
git_pre_release =
with {tag, 0} <-
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
else
_ ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
"0-g" <> String.trim(commit_hash)
end
# Branch name as pre-release version component, denoted with a dot
branch_name =
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()
|> String.replace(identifier_filter, "-")
branch_name
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
env_override = System.get_env("PLEROMA_BUILD_ENV")
env_name =
case env_override do
nil -> env_name
env_override when env_override in ["", "prod"] -> nil
env_override -> env_override
end
# Pre-release version, denoted by appending a hyphen
# and a series of dot separated identifiers
pre_release =
[git_pre_release, branch_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "-" <> String.replace(string, identifier_filter, "-")
end).()
# Build metadata, denoted with a plus sign
build_metadata =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "+" <> String.replace(string, identifier_filter, "-")
end).()
[version, pre_release, build_metadata]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
end
diff --git a/mix.lock b/mix.lock
index 158a87e47..12ce1afac 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,112 +1,112 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "95e8188490e97505c56636c1379ffdf036c1fdde", [ref: "95e8188490e97505c56636c1379ffdf036c1fdde"]},
"base62": {:hex, :base62, "1.2.1", "4866763e08555a7b3917064e9eef9194c41667276c51b59de2bc42c6ea65f806", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "3b29948de2013d3f93aa898c884a9dff847e7aec75d9d6d8c1dc4c61c2716c42"},
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bbcode": {:hex, :bbcode, "0.1.1", "0023e2c7814119b2e620b7add67182e3f6019f92bfec9a22da7e99821aceba70", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5a981b98ac7d366a9b6bf40eac389aaf4d6e623c631e6b6f8a6b571efaafd338"},
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "3aadb1e605747122f60aa7b0b121cca23c14868558157563b3f3e19ea929f7d0"},
"calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "738d0e17a93c2ccfe4ddc707bdc8e672e9074c8569498483feb1c4530fb91b2b"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]},
"castore": {:hex, :castore, "0.1.5", "591c763a637af2cc468a72f006878584bc6c306f8d111ef8ba1d4c10e0684010", [:mix], [], "hexpm", "6db356b2bc6cc22561e051ff545c20ad064af57647e436650aa24d7d06cd941a"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm", "d8700a0ca4dbb616c22c9b3f6dd539d88deaafec3efe66869d6370c9a559b3e9"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9af027d20dc12dd0c4345a6b87247e0c62965871feea0bfecf9764648b02cc69"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0bbd3222607ccaaac5c0340f7f525c627ae4d7aee6c8c8c108922620c5b6446"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
- "db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"},
+ "db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
- "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
- "ecto": {:hex, :ecto, "3.3.3", "0830bf3aebcbf3d8c1a1811cd581773b6866886c012f52c0f027031fa96a0b53", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12e368e3c2a2938d7776defaabdae40e82900fc4d8d66120ec1e01dfd8b93c3a"},
+ "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"},
+ "ecto": {:hex, :ecto, "3.3.3", "0830bf3aebcbf3d8c1a1811cd581773b6866886c012f52c0f027031fa96a0b53", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
- "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
- "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
+ "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+ "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.1", "1e4de2106cfbf4e837de41be41cd15813eabc722315e388f0d6bb3732cec47cd", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "06b6fde12b33bb6d65d5d3493e903ba5a56d57a72350c15285a4298338089e10"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0569f5b211b1a3b12b705fe2a9d0e237eb1360b9d76298028df2346cad13097a"},
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
- "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"},
+ "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"},
- "ex_syslogger": {:hex, :ex_syslogger, "1.5.0", "bc936ee3fd13d9e592cb4c3a1e8a55fccd33b05e3aa7b185f211f3ed263ff8f0", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.0.5", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "f3b4b184dcdd5f356b7c26c6cd72ab0918ba9dfb4061ccfaf519e562942af87b"},
- "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "151c476331d49b45601ffc45f43cb3a8beb396b02a34e3777fea0ad34ae57d89"},
+ "ex_syslogger": {:hex, :ex_syslogger, "1.5.0", "bc936ee3fd13d9e592cb4c3a1e8a55fccd33b05e3aa7b185f211f3ed263ff8f0", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.0.5", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm"},
+ "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"fast_html": {:hex, :fast_html, "1.0.3", "2cc0d4b68496266a1530e0c852cafeaede0bd10cfdee26fda50dc696c203162f", [:make, :mix], [], "hexpm", "ab3d782b639d3c4655fbaec0f9d032c91f8cab8dd791ac7469c2381bc7c32f85"},
"fast_sanitize": {:hex, :fast_sanitize, "0.1.7", "2a7cd8734c88a2de6de55022104f8a3b87f1fdbe8bbf131d9049764b53d50d0d", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f39fe8ea08fbac17487c30bf09b7d9f3e12472e51fb07a88ffeb8fd17da8ab67"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
- "floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "39b431b6330206cadee418e793177401ebedf2e86abc945ddd545aedb37dfc19"},
+ "floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.4", "f13088e1ec10ce01665cf25f5ff779e7df3f2dc71b37084976cf89d1aa124d5c", [:mix], [], "hexpm", "3c75b5ea8288e2ee7ea503ff9e30dfe4d07ad3c054576a6e60040e79a801e14d"},
"gun": {:git, "https://github.com/ninenines/gun.git", "bd6425ab87428cf4c95f4d23e0a48fd065fbd714", [ref: "bd6425ab87428cf4c95f4d23e0a48fd065fbd714"]},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
- "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
+ "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
- "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
+ "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
- "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
- "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
+ "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
+ "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm"},
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.4", "c5862eb3b8c64237f45f586cf00c9d892ba07bb48305a43319d428ce3c2897dd", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "e6d886252f1a41f4ba06ecf2b4c8d38760b34b1c08a11c28f7397b2e03995964"},
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm", "3bc928d817974fa10cc11e6c89b9a9361e37e96dbbf3d868c41094ec05745dcd"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"},
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
- "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
+ "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "0.12.1", "695e9490c6e0edfca616d80639528e448bd29b3bff7b7dd10a56c79b00a5d7fb", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1d58d69b8b5a86e7167abbb8cc92764a66f25f12f6172052595067fc6a30a17"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
- "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
- "phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b0bb30eda478a06dbfbe96728061a93833db3861a49ccb516f839ecb08493fbb"},
+ "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
+ "phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "ebf1bfa7b3c1c850c04929afe02e2e0d7ab135e0706332c865de03e761676b1f"},
"plug": {:hex, :plug, "1.9.0", "8d7c4e26962283ff9f8f3347bd73838e2413fbc38b7bb5467d5924f68f3a5a4a", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9902eda2c52ada2a096434682e99a2493f5d06a94d6ac6bcfff9805f952350f1"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7d722581ce865a237e14da6d946f92704101740a256bd13ec91e63c0b122fc70"},
- "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.1.2", "8b0addb5908c5238fac38e442e81b6fcd32788eaa03246b4d55d147c47c5805e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
+ "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
- "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"},
+ "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"},
"prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
- "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
+ "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "e3928e1d2889a308aaf3e42755809ac21cffd77cb58eef01cbfdab4ce2fd1e21"},
- "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm", "769ddfabd0d2a16f3f9c17eb7509951e0ca4f68363fb26f2ee51a8ec4a49881a"},
+ "syslog": {:hex, :syslog, "1.0.6", "995970c9aa7feb380ac493302138e308d6e04fd57da95b439a6df5bb3bf75076", [:rebar3], [], "hexpm"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
- "tesla": {:hex, :tesla, "1.3.2", "deb92c5c9ce35e747a395ba413ca78593a4f75bf0e1545630ee2e3d34264021e", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7567704c4790e21bd9a961b56d0b6a988ff68cc4dacfe6b2106e258da1d5cdda"},
+ "tesla": {:hex, :tesla, "1.3.2", "deb92c5c9ce35e747a395ba413ca78593a4f75bf0e1545630ee2e3d34264021e", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cd66c8a1e6a9e121d1f538b01bef459334bb4029a1ffb4eeeb5e4eae0337e7b6"},
"ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "db9fbfb5ac707bc4f85a297758406340bf0358b4af737a88113c1a9eee120ac7"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
}
diff --git a/restarter/lib/pleroma.ex b/restarter/lib/pleroma.ex
index da714654c..d7817909d 100644
--- a/restarter/lib/pleroma.ex
+++ b/restarter/lib/pleroma.ex
@@ -1,28 +1,74 @@
defmodule Restarter.Pleroma do
use GenServer
+ require Logger
+
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
- def init(_), do: {:ok, %{}}
+ def init(_), do: {:ok, %{need_reboot?: false}}
- def handle_info(:after_boot, %{after_boot: true} = state), do: {:noreply, state}
+ def need_reboot? do
+ GenServer.call(__MODULE__, :need_reboot?)
+ end
- def handle_info(:after_boot, state) do
- restart(:pleroma)
- {:noreply, Map.put(state, :after_boot, true)}
+ def need_reboot do
+ GenServer.cast(__MODULE__, :need_reboot)
+ end
+
+ def refresh do
+ GenServer.cast(__MODULE__, :refresh)
+ end
+
+ def restart(env, delay) do
+ GenServer.cast(__MODULE__, {:restart, env, delay})
+ end
+
+ def restart_after_boot(env) do
+ GenServer.cast(__MODULE__, {:after_boot, env})
+ end
+
+ def handle_call(:need_reboot?, _from, state) do
+ {:reply, state[:need_reboot?], state}
+ end
+
+ def handle_cast(:refresh, _state) do
+ {:noreply, %{need_reboot?: false}}
end
- def handle_info({:restart, delay}, state) do
+ def handle_cast(:need_reboot, %{need_reboot?: true} = state), do: {:noreply, state}
+
+ def handle_cast(:need_reboot, state) do
+ {:noreply, Map.put(state, :need_reboot?, true)}
+ end
+
+ def handle_cast({:restart, :test, _}, state) do
+ Logger.warn("pleroma restarted")
+ {:noreply, Map.put(state, :need_reboot?, false)}
+ end
+
+ def handle_cast({:restart, _, delay}, state) do
Process.sleep(delay)
- restart(:pleroma)
- {:noreply, state}
+ do_restart(:pleroma)
+ {:noreply, Map.put(state, :need_reboot?, false)}
+ end
+
+ def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state}
+
+ def handle_cast({:after_boot, :test}, state) do
+ Logger.warn("pleroma restarted")
+ {:noreply, Map.put(state, :after_boot, true)}
+ end
+
+ def handle_cast({:after_boot, _}, state) do
+ do_restart(:pleroma)
+ {:noreply, Map.put(state, :after_boot, true)}
end
- defp restart(app) do
+ defp do_restart(app) do
:ok = Application.ensure_started(app)
:ok = Application.stop(app)
:ok = Application.start(app)
end
end
diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index ebdc951cf..3d7218dde 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -1,181 +1,185 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.TransferTaskTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
alias Pleroma.Config.TransferTask
alias Pleroma.ConfigDB
clear_config(:configurable_from_database) do
Pleroma.Config.put(:configurable_from_database, true)
end
test "transfer config values from db to env" do
refute Application.get_env(:pleroma, :test_key)
refute Application.get_env(:idna, :test_key)
refute Application.get_env(:quack, :test_key)
ConfigDB.create(%{
group: ":pleroma",
key: ":test_key",
value: [live: 2, com: 3]
})
ConfigDB.create(%{
group: ":idna",
key: ":test_key",
value: [live: 15, com: 35]
})
ConfigDB.create(%{
group: ":quack",
key: ":test_key",
value: [:test_value1, :test_value2]
})
TransferTask.start_link([])
assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
on_exit(fn ->
Application.delete_env(:pleroma, :test_key)
Application.delete_env(:idna, :test_key)
Application.delete_env(:quack, :test_key)
end)
end
test "transfer config values for 1 group and some keys" do
level = Application.get_env(:quack, :level)
meta = Application.get_env(:quack, :meta)
ConfigDB.create(%{
group: ":quack",
key: ":level",
value: :info
})
ConfigDB.create(%{
group: ":quack",
key: ":meta",
value: [:none]
})
TransferTask.start_link([])
assert Application.get_env(:quack, :level) == :info
assert Application.get_env(:quack, :meta) == [:none]
default = Pleroma.Config.Holder.config(:quack, :webhook_url)
assert Application.get_env(:quack, :webhook_url) == default
on_exit(fn ->
Application.put_env(:quack, :level, level)
Application.put_env(:quack, :meta, meta)
end)
end
test "transfer config values with full subkey update" do
emoji = Application.get_env(:pleroma, :emoji)
assets = Application.get_env(:pleroma, :assets)
ConfigDB.create(%{
group: ":pleroma",
key: ":emoji",
value: [groups: [a: 1, b: 2]]
})
ConfigDB.create(%{
group: ":pleroma",
key: ":assets",
value: [mascots: [a: 1, b: 2]]
})
TransferTask.start_link([])
emoji_env = Application.get_env(:pleroma, :emoji)
assert emoji_env[:groups] == [a: 1, b: 2]
assets_env = Application.get_env(:pleroma, :assets)
assert assets_env[:mascots] == [a: 1, b: 2]
on_exit(fn ->
Application.put_env(:pleroma, :emoji, emoji)
Application.put_env(:pleroma, :assets, assets)
end)
end
describe "pleroma restart" do
+ setup do
+ on_exit(fn -> Restarter.Pleroma.refresh() end)
+ end
+
test "don't restart if no reboot time settings were changed" do
emoji = Application.get_env(:pleroma, :emoji)
on_exit(fn -> Application.put_env(:pleroma, :emoji, emoji) end)
ConfigDB.create(%{
group: ":pleroma",
key: ":emoji",
value: [groups: [a: 1, b: 2]]
})
refute String.contains?(
capture_log(fn -> TransferTask.start_link([]) end),
"pleroma restarted"
)
end
- test "restart pleroma on reboot time key" do
+ test "on reboot time key" do
chat = Application.get_env(:pleroma, :chat)
on_exit(fn -> Application.put_env(:pleroma, :chat, chat) end)
ConfigDB.create(%{
group: ":pleroma",
key: ":chat",
value: [enabled: false]
})
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
- test "restart pleroma on reboot time subkey" do
+ test "on reboot time subkey" do
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn -> Application.put_env(:pleroma, Pleroma.Captcha, captcha) end)
ConfigDB.create(%{
group: ":pleroma",
key: "Pleroma.Captcha",
value: [seconds_valid: 60]
})
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
test "don't restart pleroma on reboot time key and subkey if there is false flag" do
chat = Application.get_env(:pleroma, :chat)
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn ->
Application.put_env(:pleroma, :chat, chat)
Application.put_env(:pleroma, Pleroma.Captcha, captcha)
end)
ConfigDB.create(%{
group: ":pleroma",
key: ":chat",
value: [enabled: false]
})
ConfigDB.create(%{
group: ":pleroma",
key: "Pleroma.Captcha",
value: [seconds_valid: 60]
})
refute String.contains?(
capture_log(fn -> TransferTask.load_and_update_env([], false) end),
"pleroma restarted"
)
end
end
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 02ffbfa0b..5b0c29439 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,3478 +1,3556 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
+ import Pleroma.Factory
+ import ExUnit.CaptureLog
+
alias Pleroma.Activity
+ alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
- import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
- Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
+ Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
end
test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
- Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
+ Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
end
test "GET /api/pleroma/admin/users/:nickname requires " <>
"read:accounts or admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
good_token5 = insert(:oauth_token, user: admin, scopes: ["read"])
good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5]
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "DELETE /api/pleroma/admin/users" do
test "single user", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == user.nickname
end
test "multiple users", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users", %{
nicknames: [user_one.nickname, user_two.nickname]
})
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
response = json_response(conn, 200)
assert response -- [user_one.nickname, user_two.nickname] == []
end
end
describe "/api/pleroma/admin/users" do
test "Create", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
})
response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
assert response == ["success", "success"]
log_entry = Repo.one(ModerationLog)
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
}
]
end
test "Cannot create user with existing nickname", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => "someuser@plerama.social",
"nickname" => user.nickname
},
"error" => "nickname has already been taken",
"type" => "error"
}
]
end
test "Multiple user creation works in transaction", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
},
%{
"code" => 409,
"data" => %{
"email" => "newuser@pleroma.social",
"nickname" => "newuser"
},
"error" => "",
"type" => "error"
}
]
assert User.get_by_nickname("newuser") === nil
end
end
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
expected = %{
"deactivated" => false,
"id" => to_string(user.id),
"local" => true,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
assert expected == json_response(conn, 200)
end
test "when the user doesn't exist", %{conn: conn} do
user = build(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert "Not found" == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
test "allows to force-follow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end
end
describe "/api/pleroma/admin/users/unfollow" do
test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end
end
describe "PUT /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
conn
|> put_req_header("accept", "application/json")
|> put(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
"#{user2.nickname}&tags[]=foo&tags[]=bar"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it appends specified tags to users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "DELETE /api/pleroma/admin/users/tag" do
setup %{conn: conn} do
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
conn
|> put_req_header("accept", "application/json")
|> delete(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <>
"#{user2.nickname}&tags[]=x&tags[]=z"
)
%{conn: conn, user1: user1, user2: user2, user3: user3}
end
test "it removes specified tags from users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["x", "z"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
test "GET is giving user_info", %{admin: admin, conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
assert json_response(conn, 200) == %{
"is_admin" => true,
"is_moderator" => false
}
end
test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => true
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user.nickname} admin"
end
test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
assert json_response(conn, 200) == %{"is_admin" => true}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do
user = insert(:user, is_admin: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
test "/:right DELETE, can remove from a permission group (multiple)", %{
admin: admin,
conn: conn
} do
user_one = insert(:user, is_admin: true)
user_two = insert(:user, is_admin: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
assert json_response(conn, 200) == %{"is_admin" => false}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
user_two.nickname
}"
end
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
clear_config([:instance, :registrations_open]) do
- Pleroma.Config.put([:instance, :registrations_open], false)
+ Config.put([:instance, :registrations_open], false)
end
clear_config([:instance, :invites_enabled]) do
- Pleroma.Config.put([:instance, :invites_enabled], true)
+ Config.put([:instance, :invites_enabled], true)
end
test "sends invitation and returns 204", %{admin: admin, conn: conn} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
post(
conn,
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)
assert json_response(conn, :no_content)
token_record = List.last(Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
- notify_email = Pleroma.Config.get([:instance, :notify_email])
- instance_name = Pleroma.Config.get([:instance, :name])
+ notify_email = Config.get([:instance, :notify_email])
+ instance_name = Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
admin,
token_record,
recipient_email,
recipient_name
)
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {recipient_name, recipient_email},
html_body: email.html_body
)
end
test "it returns 403 if requested by a non-admin" do
non_admin_user = insert(:user)
token = insert(:oauth_token, user: non_admin_user)
conn =
build_conn()
|> assign(:user, non_admin_user)
|> assign(:token, token)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
end
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
clear_config([:instance, :registrations_open])
clear_config([:instance, :invites_enabled])
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
- Pleroma.Config.put([:instance, :registrations_open], false)
- Pleroma.Config.put([:instance, :invites_enabled], false)
+ Config.put([:instance, :registrations_open], false)
+ Config.put([:instance, :invites_enabled], false)
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
- Pleroma.Config.put([:instance, :registrations_open], true)
- Pleroma.Config.put([:instance, :invites_enabled], true)
+ Config.put([:instance, :registrations_open], true)
+ Config.put([:instance, :invites_enabled], true)
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
end
test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
resp = json_response(conn, 200)
assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "GET /api/pleroma/admin/users" do
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
conn = get(conn, "/api/pleroma/admin/users?page=1")
users =
[
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => false,
"tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?page=2")
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => []
}
end
test "regular search", %{conn: conn} do
user = insert(:user, nickname: "bob")
conn = get(conn, "/api/pleroma/admin/users?query=bo")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by domain", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by full nickname", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by display name", %{conn: conn} do
user = insert(:user, name: "Display name")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?name=display")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "search by email", %{conn: conn} do
user = insert(:user, email: "email@example.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?email=email@example.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "regular search with page size", %{conn: conn} do
user = insert(:user, nickname: "aalice")
user2 = insert(:user, nickname: "alice")
conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1")
assert json_response(conn1, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2")
assert json_response(conn2, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
"deactivated" => user2.deactivated,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
}
end
test "only local users" do
admin = insert(:user, is_admin: true, nickname: "john")
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "only local users with no query", %{conn: conn, admin: old_admin} do
admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn = get(conn, "/api/pleroma/admin/users?filters=local")
users =
[
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => old_admin.id,
"local" => true,
"nickname" => old_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "load only admins", %{conn: conn, admin: admin} do
second_admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_admin")
users =
[
%{
"deactivated" => false,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => admin.local,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => second_admin.id,
"nickname" => second_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => second_admin.local,
"tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "load only moderators", %{conn: conn} do
moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => false,
"id" => moderator.id,
"nickname" => moderator.nickname,
"roles" => %{"admin" => false, "moderator" => true},
"local" => moderator.local,
"tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
"confirmation_pending" => false
}
]
}
end
test "load users with tags list", %{conn: conn} do
user1 = insert(:user, tags: ["first"])
user2 = insert(:user, tags: ["second"])
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second")
users =
[
%{
"deactivated" => false,
"id" => user1.id,
"nickname" => user1.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user1.local,
"tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user1.name || user1.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user2.local,
"tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "it works with multiple filters" do
admin = insert(:user, nickname: "john", is_admin: true)
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob", local: false, deactivated: true)
insert(:user, nickname: "ken", local: true, deactivated: true)
insert(:user, nickname: "bobb", local: false, deactivated: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
end
test "it omits relay user", %{admin: admin, conn: conn} do
assert %User{} = Relay.get_actor()
conn = get(conn, "/api/pleroma/admin/users")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}
]
}
end
end
test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: true)
user_two = insert(:user, deactivated: true)
conn =
patch(
conn,
"/api/pleroma/admin/users/activate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: false)
user_two = insert(:user, deactivated: false)
conn =
patch(
conn,
"/api/pleroma/admin/users/deactivate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user)
conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
%{
"deactivated" => !user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
describe "POST /api/pleroma/admin/users/invite_token" do
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
refute invite.max_use
assert invite.invite_type == "one_time"
end
test "with expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
refute invite.max_use
assert invite.invite_type == "date_limited"
end
test "with max_use", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
assert invite.max_use == 150
assert invite.invite_type == "reusable"
end
test "with max use and expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"max_use" => 150,
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
assert invite.max_use == 150
assert invite.invite_type == "reusable_date_limited"
end
end
describe "GET /api/pleroma/admin/users/invites" do
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{"invites" => []}
end
test "with invite", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{
"invites" => [
%{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => false,
"uses" => 0
}
]
}
end
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
test "with token", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => true,
"uses" => 0
}
end
test "with invalid token", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports/:id" do
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports/#{report_id}")
|> json_response(:ok)
assert response["id"] == report_id
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/reports/test")
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PATCH /api/pleroma/admin/reports" do
setup do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel very offended",
"status_ids" => [activity.id]
})
%{
id: report_id,
second_report_id: second_report_id
}
end
test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do
read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"])
write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"])
response =
conn
|> assign(:token, read_token)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(403)
assert response == %{
"error" => "Insufficient permissions: admin:write:reports."
}
conn
|> assign(:token, write_token)
|> patch("/api/pleroma/admin/reports", %{
"reports" => [%{"state" => "resolved", "id" => id}]
})
|> json_response(:no_content)
end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
assert activity.data["state"] == "resolved"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
end
test "closes report", %{conn: conn, id: id, admin: admin} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
assert activity.data["state"] == "closed"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'closed' state"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "test", "id" => id}
]
})
assert hd(json_response(conn, :bad_request))["error"] == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "closed", "id" => "test"}
]
})
assert hd(json_response(conn, :bad_request))["error"] == "not_found"
end
test "updates state of multiple reports", %{
conn: conn,
id: id,
admin: admin,
second_report_id: second_report_id
} do
conn
|> patch("/api/pleroma/admin/reports", %{
"reports" => [
%{"state" => "resolved", "id" => id},
%{"state" => "closed", "id" => second_report_id}
]
})
|> json_response(:no_content)
activity = Activity.get_by_id(id)
second_activity = Activity.get_by_id(second_report_id)
assert activity.data["state"] == "resolved"
assert second_activity.data["state"] == "closed"
[first_log_entry, second_log_entry] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(first_log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
assert ModerationLog.get_log_entry_message(second_log_entry) ==
"@#{admin.nickname} updated report ##{second_report_id} with 'closed' state"
end
end
describe "GET /api/pleroma/admin/reports" do
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns reports", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
[report] = response["reports"]
assert length(response["reports"]) == 1
assert report["id"] == report_id
assert response["total"] == 1
end
test "returns reports with specified state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I don't like this user"
})
CommonAPI.update_report_state(second_report_id, "closed")
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "open"
})
|> json_response(:ok)
[open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "closed"
})
|> json_response(:ok)
[closed_report] = response["reports"]
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
token = insert(:oauth_token, user: user)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, token)
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) ==
%{"error" => "User is not an admin or OAuth admin scope is not granted."}
end
test "returns 403 when requested by anonymous" do
conn = get(build_conn(), "/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
describe "GET /api/pleroma/admin/grouped_reports" do
setup do
[reporter, target_user] = insert_pair(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
first_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date1})
second_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date2})
third_status =
insert(:note_activity, user: target_user, data_attrs: %{"published" => date3})
{:ok, first_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id, third_status.id]
})
{:ok, second_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id, second_status.id]
})
{:ok, third_report} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"status_ids" => [first_status.id]
})
%{
first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]),
second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]),
third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]),
first_report: first_report,
first_status_reports: [first_report, second_report, third_report],
second_status_reports: [first_report, second_report],
third_status_reports: [first_report],
target_user: target_user,
reporter: reporter
}
end
test "returns reports grouped by status", %{
conn: conn,
first_status: first_status,
second_status: second_status,
third_status: third_status,
first_status_reports: first_status_reports,
second_status_reports: second_status_reports,
third_status_reports: third_status_reports,
target_user: target_user,
reporter: reporter
} do
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
assert length(response["reports"]) == 3
first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
second_group = Enum.find(response["reports"], &(&1["status"]["id"] == second_status.id))
third_group = Enum.find(response["reports"], &(&1["status"]["id"] == third_status.id))
assert length(first_group["reports"]) == 3
assert length(second_group["reports"]) == 2
assert length(third_group["reports"]) == 1
assert first_group["date"] ==
Enum.max_by(first_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert first_group["status"] ==
Map.put(
stringify_keys(StatusView.render("show.json", %{activity: first_status})),
"deleted",
false
)
assert(first_group["account"]["id"] == target_user.id)
assert length(first_group["actors"]) == 1
assert hd(first_group["actors"])["id"] == reporter.id
assert Enum.map(first_group["reports"], & &1["id"]) --
Enum.map(first_status_reports, & &1.id) == []
assert second_group["date"] ==
Enum.max_by(second_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert second_group["status"] ==
Map.put(
stringify_keys(StatusView.render("show.json", %{activity: second_status})),
"deleted",
false
)
assert second_group["account"]["id"] == target_user.id
assert length(second_group["actors"]) == 1
assert hd(second_group["actors"])["id"] == reporter.id
assert Enum.map(second_group["reports"], & &1["id"]) --
Enum.map(second_status_reports, & &1.id) == []
assert third_group["date"] ==
Enum.max_by(third_status_reports, fn act ->
NaiveDateTime.from_iso8601!(act.data["published"])
end).data["published"]
assert third_group["status"] ==
Map.put(
stringify_keys(StatusView.render("show.json", %{activity: third_status})),
"deleted",
false
)
assert third_group["account"]["id"] == target_user.id
assert length(third_group["actors"]) == 1
assert hd(third_group["actors"])["id"] == reporter.id
assert Enum.map(third_group["reports"], & &1["id"]) --
Enum.map(third_status_reports, & &1.id) == []
end
test "reopened report renders status data", %{
conn: conn,
first_report: first_report,
first_status: first_status
} do
{:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
first_group = Enum.find(response["reports"], &(&1["status"]["id"] == first_status.id))
assert first_group["status"] ==
Map.put(
stringify_keys(StatusView.render("show.json", %{activity: first_status})),
"deleted",
false
)
end
test "reopened report does not render status data if status has been deleted", %{
conn: conn,
first_report: first_report,
first_status: first_status,
target_user: target_user
} do
{:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
{:ok, _} = CommonAPI.delete(first_status.id, target_user)
refute Activity.get_by_ap_id(first_status.id)
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["status"][
"deleted"
] == true
assert length(Enum.filter(response["reports"], &(&1["status"]["deleted"] == false))) == 2
end
test "account not empty if status was deleted", %{
conn: conn,
first_report: first_report,
first_status: first_status,
target_user: target_user
} do
{:ok, _} = CommonAPI.update_report_state(first_report.id, "resolved")
{:ok, _} = CommonAPI.delete(first_status.id, target_user)
refute Activity.get_by_ap_id(first_status.id)
response =
conn
|> get("/api/pleroma/admin/grouped_reports")
|> json_response(:ok)
assert Enum.find(response["reports"], &(&1["status"]["deleted"] == true))["account"]
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
setup do
activity = insert(:note_activity)
%{id: activity.id}
end
test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response(:ok)
assert response["sensitive"]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set sensitive: 'true'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|> json_response(:ok)
refute response["sensitive"]
end
test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|> json_response(:ok)
assert response["visibility"] == "public"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set visibility: 'public'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
|> json_response(:ok)
assert response["visibility"] == "private"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"})
|> json_response(:ok)
assert response["visibility"] == "unlisted"
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
conn = put(conn, "/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
setup do
activity = insert(:note_activity)
%{id: activity.id}
end
test "deletes status", %{conn: conn, id: id, admin: admin} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted status ##{id}"
end
test "returns error when status is not exist", %{conn: conn} do
conn = delete(conn, "/api/pleroma/admin/statuses/test")
assert json_response(conn, :bad_request) == "Could not delete"
end
end
describe "GET /api/pleroma/admin/config" do
clear_config(:configurable_from_database) do
- Pleroma.Config.put(:configurable_from_database, true)
+ Config.put(:configurable_from_database, true)
end
test "when configuration from database is off", %{conn: conn} do
- initial = Pleroma.Config.get(:configurable_from_database)
- Pleroma.Config.put(:configurable_from_database, false)
- on_exit(fn -> Pleroma.Config.put(:configurable_from_database, initial) end)
+ initial = Config.get(:configurable_from_database)
+ Config.put(:configurable_from_database, false)
+ on_exit(fn -> Config.put(:configurable_from_database, initial) end)
conn = get(conn, "/api/pleroma/admin/config")
assert json_response(conn, 400) ==
"To use this endpoint you need to enable configuration from database."
end
test "with settings only in db", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true})
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => key1,
"value" => _
},
%{
"group" => ":pleroma",
"key" => key2,
"value" => _
}
]
} = json_response(conn, 200)
assert key1 == config1.key
assert key2 == config2.key
end
test "db is added to settings that are in db", %{conn: conn} do
_config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name"))
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
[instance_config] =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key == ":instance"
end)
assert instance_config["db"] == [":name"]
end
test "merged default setting with db settings", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
config3 =
insert(:config,
value: ConfigDB.to_binary(k1: :v1, k2: :v2)
)
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
assert length(configs) > 3
received_configs =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key in [config1.key, config2.key, config3.key]
end)
assert length(received_configs) == 3
db_keys =
config3.value
|> ConfigDB.from_binary()
|> Keyword.keys()
|> ConfigDB.convert()
Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
assert db in [[config1.key], [config2.key], db_keys]
assert value in [
ConfigDB.from_binary_with_convert(config1.value),
ConfigDB.from_binary_with_convert(config2.value),
ConfigDB.from_binary_with_convert(config3.value)
]
end)
end
test "subkeys with full update right merge", %{conn: conn} do
config1 =
insert(:config,
key: ":emoji",
value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1])
)
config2 =
insert(:config,
key: ":assets",
value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1])
)
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response(200)
vals =
Enum.filter(configs, fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key in [config1.key, config2.key]
end)
emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
emoji_val = ConfigDB.transform_with_out_binary(emoji["value"])
assets_val = ConfigDB.transform_with_out_binary(assets["value"])
assert emoji_val[:groups] == [a: 1, b: 2]
assert assets_val[:mascots] == [a: 1, b: 2]
end
end
test "POST /api/pleroma/admin/config error", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []})
assert json_response(conn, 400) ==
"To use this endpoint you need to enable configuration from database."
end
describe "POST /api/pleroma/admin/config" do
setup do
http = Application.get_env(:pleroma, :http)
on_exit(fn ->
Application.delete_env(:pleroma, :key1)
Application.delete_env(:pleroma, :key2)
Application.delete_env(:pleroma, :key3)
Application.delete_env(:pleroma, :key4)
Application.delete_env(:pleroma, :keyaa1)
Application.delete_env(:pleroma, :keyaa2)
Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
Application.put_env(:pleroma, :http, http)
Application.put_env(:tesla, :adapter, Tesla.Mock)
+ Restarter.Pleroma.refresh()
end)
end
clear_config(:configurable_from_database) do
- Pleroma.Config.put(:configurable_from_database, true)
+ Config.put(:configurable_from_database, true)
end
@tag capture_log: true
test "create new config setting in db", %{conn: conn} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: ":pleroma", key: ":key1", value: "value1"},
%{
group: ":ueberauth",
key: "Ueberauth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
group: ":pleroma",
key: ":key2",
value: %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
}
},
%{
group: ":pleroma",
key: ":key3",
value: [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
group: ":pleroma",
key: ":key4",
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
group: ":idna",
key: ":key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => "value1",
"db" => [":key1"]
},
%{
"group" => ":ueberauth",
"key" => "Ueberauth",
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
"db" => [":consumer_secret"]
},
%{
"group" => ":pleroma",
"key" => ":key2",
"value" => %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
},
"db" => [":key2"]
},
%{
"group" => ":pleroma",
"key" => ":key3",
"value" => [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
],
"db" => [":key3"]
},
%{
"group" => ":pleroma",
"key" => ":key4",
"value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
"db" => [":key4"]
},
%{
"group" => ":idna",
"key" => ":key5",
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
"db" => [":key5"]
}
]
}
assert Application.get_env(:pleroma, :key1) == "value1"
assert Application.get_env(:pleroma, :key2) == %{
nested_1: "nested_value1",
nested_2: [
%{nested_22: "nested_value222"},
%{nested_33: %{nested_44: "nested_444"}}
]
}
assert Application.get_env(:pleroma, :key3) == [
%{"nested_3" => :nested_3, "nested_33" => "nested_33"},
%{"nested_4" => true}
]
assert Application.get_env(:pleroma, :key4) == %{
"endpoint" => "https://example.com",
nested_5: :upload
}
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
test "save configs setting without explicit key", %{conn: conn} do
level = Application.get_env(:quack, :level)
meta = Application.get_env(:quack, :meta)
webhook_url = Application.get_env(:quack, :webhook_url)
on_exit(fn ->
Application.put_env(:quack, :level, level)
Application.put_env(:quack, :meta, meta)
Application.put_env(:quack, :webhook_url, webhook_url)
end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":quack",
key: ":level",
value: ":info"
},
%{
group: ":quack",
key: ":meta",
value: [":none"]
},
%{
group: ":quack",
key: ":webhook_url",
value: "https://hooks.slack.com/services/KEY"
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":quack",
"key" => ":level",
"value" => ":info",
"db" => [":level"]
},
%{
"group" => ":quack",
"key" => ":meta",
"value" => [":none"],
"db" => [":meta"]
},
%{
"group" => ":quack",
"key" => ":webhook_url",
"value" => "https://hooks.slack.com/services/KEY",
"db" => [":webhook_url"]
}
]
}
assert Application.get_env(:quack, :level) == :info
assert Application.get_env(:quack, :meta) == [:none]
assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY"
end
test "saving config with partial update", %{conn: conn} do
config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key1", 1]},
%{"tuple" => [":key2", 2]},
%{"tuple" => [":key3", 3]}
],
"db" => [":key1", ":key2", ":key3"]
}
]
}
end
test "saving config which need pleroma reboot", %{conn: conn} do
- chat = Pleroma.Config.get(:chat)
- on_exit(fn -> Pleroma.Config.put(:chat, chat) end)
+ chat = Config.get(:chat)
+ on_exit(fn -> Config.put(:chat, chat) end)
- conn =
- post(
- conn,
- "/api/pleroma/admin/config",
- %{
- configs: [
- %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
- ]
- }
- )
+ assert post(
+ conn,
+ "/api/pleroma/admin/config",
+ %{
+ configs: [
+ %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
+ ]
+ }
+ )
+ |> json_response(200) == %{
+ "configs" => [
+ %{
+ "db" => [":enabled"],
+ "group" => ":pleroma",
+ "key" => ":chat",
+ "value" => [%{"tuple" => [":enabled", true]}]
+ }
+ ],
+ "need_reboot" => true
+ }
- assert json_response(conn, 200) == %{
+ configs =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ assert configs["need_reboot"]
+
+ capture_log(fn ->
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
+ end) =~ "pleroma restarted"
+
+ configs =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ refute Map.has_key?(configs, "need_reboot")
+ end
+
+ test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do
+ chat = Config.get(:chat)
+ on_exit(fn -> Config.put(:chat, chat) end)
+
+ assert post(
+ conn,
+ "/api/pleroma/admin/config",
+ %{
+ configs: [
+ %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
+ ]
+ }
+ )
+ |> json_response(200) == %{
"configs" => [
%{
"db" => [":enabled"],
"group" => ":pleroma",
"key" => ":chat",
"value" => [%{"tuple" => [":enabled", true]}]
}
],
"need_reboot" => true
}
+
+ assert post(conn, "/api/pleroma/admin/config", %{
+ configs: [
+ %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]}
+ ]
+ })
+ |> json_response(200) == %{
+ "configs" => [
+ %{
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [
+ %{"tuple" => [":key3", 3]}
+ ],
+ "db" => [":key3"]
+ }
+ ],
+ "need_reboot" => true
+ }
+
+ capture_log(fn ->
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
+ end) =~ "pleroma restarted"
+
+ configs =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response(200)
+
+ refute Map.has_key?(configs, "need_reboot")
end
test "saving config with nested merge", %{conn: conn} do
config =
insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
value: [
%{"tuple" => [":key3", 3]},
%{
"tuple" => [
":key2",
[
%{"tuple" => [":k2", 1]},
%{"tuple" => [":k3", 3]}
]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key1", 1]},
%{"tuple" => [":key3", 3]},
%{
"tuple" => [
":key2",
[
%{"tuple" => [":k1", 1]},
%{"tuple" => [":k2", 1]},
%{"tuple" => [":k3", 3]}
]
]
}
],
"db" => [":key1", ":key3", ":key2"]
}
]
}
end
test "saving special atoms", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{
"tuple" => [
":ssl_options",
[%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{
"tuple" => [
":ssl_options",
[%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}]
]
}
],
"db" => [":ssl_options"]
}
]
}
assert Application.get_env(:pleroma, :key1) == [
ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]]
]
end
test "saving full setting if value is in full_key_update list", %{conn: conn} do
backends = Application.get_env(:logger, :backends)
on_exit(fn -> Application.put_env(:logger, :backends, backends) end)
config =
insert(:config,
group: ":logger",
key: ":backends",
value: :erlang.term_to_binary([])
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
value: [":console", %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":logger",
"key" => ":backends",
"value" => [
":console",
%{"tuple" => ["ExSyslogger", ":ex_syslogger"]}
],
"db" => [":backends"]
}
]
}
assert Application.get_env(:logger, :backends) == [
:console,
{ExSyslogger, :ex_syslogger}
]
- ExUnit.CaptureLog.capture_log(fn ->
+ capture_log(fn ->
require Logger
Logger.warn("Ooops...")
end) =~ "Ooops..."
end
test "saving full setting if value is not keyword", %{conn: conn} do
config =
insert(:config,
group: ":tesla",
key: ":adapter",
value: :erlang.term_to_binary(Tesla.Adapter.Hackey)
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":tesla",
"key" => ":adapter",
"value" => "Tesla.Adapter.Httpc",
"db" => [":adapter"]
}
],
"need_reboot" => true
}
end
test "update config setting & delete with fallback to default value", %{
conn: conn,
admin: admin,
token: token
} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
config1 = insert(:config, key: ":keyaa1")
config2 = insert(:config, key: ":keyaa2")
config3 =
insert(:config,
group: ":ueberauth",
key: "Ueberauth"
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config1.group, key: config1.key, value: "another_value"},
%{group: config2.group, key: config2.key, value: "another_value"}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => config1.key,
"value" => "another_value",
"db" => [":keyaa1"]
},
%{
"group" => ":pleroma",
"key" => config2.key,
"value" => "another_value",
"db" => [":keyaa2"]
}
]
}
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
assert Application.get_env(:pleroma, :keyaa2) == "another_value"
assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> post("/api/pleroma/admin/config", %{
configs: [
%{group: config2.group, key: config2.key, delete: true},
%{
group: ":ueberauth",
key: "Ueberauth",
delete: true
}
]
})
assert json_response(conn, 200) == %{
"configs" => []
}
assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
end
test "common config example", %{conn: conn} do
adapter = Application.get_env(:tesla, :adapter)
on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]},
%{"tuple" => [":name", "Pleroma"]}
]
},
%{
"group" => ":tesla",
"key" => ":adapter",
"value" => "Tesla.Adapter.Httpc"
}
]
})
assert Application.get_env(:tesla, :adapter) == Tesla.Adapter.Httpc
- assert Pleroma.Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
+ assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma"
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]},
%{"tuple" => [":name", "Pleroma"]}
],
"db" => [
":enabled",
":method",
":seconds_valid",
":path",
":key1",
":regex1",
":regex2",
":regex3",
":regex4",
":name"
]
},
%{
"group" => ":tesla",
"key" => ":adapter",
"value" => "Tesla.Adapter.Httpc",
"db" => [":adapter"]
}
],
"need_reboot" => true
}
end
test "tuples with more than two values", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
],
"db" => [":http"]
}
]
}
end
test "settings with nesting map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000,
"nested" => %{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000
}
}
]
}
]
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0,
"nested" => %{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0
}
}
]
}
],
"db" => [":key2", ":key3"]
}
]
}
end
test "value as map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"},
"db" => [":key1"]
}
]
}
end
test "queues key as atom", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
],
"db" => [
":federator_incoming",
":federator_outgoing",
":web_push",
":mailer",
":transmogrifier",
":scheduled_activities",
":background"
]
}
]
}
end
test "delete part of settings by atom subkeys", %{conn: conn} do
config =
insert(:config,
key: ":keyaa1",
value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
subkeys: [":subkey1", ":subkey3"],
delete: true
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":keyaa1",
"value" => [%{"tuple" => [":subkey2", "val2"]}],
"db" => [":subkey2"]
}
]
}
end
test "proxy tuple localhost", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
test "proxy tuple domain", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
test "proxy tuple ip", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: ":pleroma",
key: ":http",
value: [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => ":pleroma",
"key" => ":http",
"value" => [
%{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]},
%{"tuple" => [":send_user_agent", false]}
],
"db" => [":proxy_url", ":send_user_agent"]
}
]
}
end
end
describe "GET /api/pleroma/admin/restart" do
clear_config(:configurable_from_database) do
- Pleroma.Config.put(:configurable_from_database, true)
+ Config.put(:configurable_from_database, true)
end
test "pleroma restarts", %{conn: conn} do
- ExUnit.CaptureLog.capture_log(fn ->
+ capture_log(fn ->
assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
end) =~ "pleroma restarted"
+
+ refute Restarter.Pleroma.need_reboot?()
end
end
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
insert(:note_activity, user: user, published: date1)
insert(:note_activity, user: user, published: date2)
insert(:note_activity, user: user, published: date3)
%{user: user}
end
test "renders user's statuses", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 3
end
test "renders user's statuses with a limit", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2")
assert json_response(conn, 200) |> length() == 2
end
test "doesn't return private statuses by default", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 4
end
test "returns private statuses with godmode on", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
assert json_response(conn, 200) |> length() == 5
end
end
describe "GET /api/pleroma/admin/moderation_log" do
setup do
moderator = insert(:user, is_moderator: true)
%{moderator: moderator}
end
test "returns the log", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn = get(conn, "/api/pleroma/admin/moderation_log")
response = json_response(conn, 200)
[first_entry, second_entry] = response["items"]
assert response["total"] == 2
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "returns the log with pagination", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 2
assert response1["items"] |> length() == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
response2 = json_response(conn2, 200)
[second_entry] = response2["items"]
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
test "sets password_reset_pending to true", %{conn: conn} do
user = insert(:user)
assert user.password_reset_pending == false
conn =
patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]})
assert json_response(conn, 204) == ""
ObanHelpers.perform_all()
assert User.get_by_id(user.id).password_reset_pending == true
end
end
describe "relays" do
test "POST /relay", %{conn: conn, admin: admin} do
conn =
post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
end
test "GET /relay", %{conn: conn} do
relay_user = Pleroma.Web.ActivityPub.Relay.get_actor()
["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"]
|> Enum.each(fn ap_id ->
{:ok, user} = User.get_or_fetch_by_ap_id(ap_id)
User.follow(relay_user, user)
end)
conn = get(conn, "/api/pleroma/admin/relay")
assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == []
end
test "DELETE /relay", %{conn: conn, admin: admin} do
post(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
conn =
delete(conn, "/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
[log_entry_one, log_entry_two] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry_one) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
assert ModerationLog.get_log_entry_message(log_entry_two) ==
"@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
end
end
describe "instances" do
test "GET /instances/:instance/statuses", %{conn: conn} do
user = insert(:user, local: false, nickname: "archaeme@archae.me")
user2 = insert(:user, local: false, nickname: "test@test.com")
insert_pair(:note_activity, user: user)
insert(:note_activity, user: user2)
ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses")
response = json_response(ret_conn, 200)
assert length(response) == 2
ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses")
response = json_response(ret_conn, 200)
assert length(response) == 1
ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses")
response = json_response(ret_conn, 200)
assert Enum.empty?(response)
end
end
describe "PATCH /confirm_email" do
test "it confirms emails of two users", %{conn: conn, admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
ret_conn =
patch(conn, "/api/pleroma/admin/users/confirm_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert ret_conn.status == 200
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
describe "PATCH /resend_confirmation_email" do
test "it resend emails for two users", %{conn: conn, admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
ret_conn =
patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert ret_conn.status == 200
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
describe "POST /reports/:id/notes" do
setup %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting!"
})
post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting2!"
})
%{
admin_id: admin.id,
report_id: report_id
}
end
test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
[note, _] = Repo.all(ReportNote)
assert %{
activity_id: ^report_id,
content: "this is disgusting!",
user_id: ^admin_id
} = note
end
test "it returns reports with notes", %{conn: conn, admin: admin} do
conn = get(conn, "/api/pleroma/admin/reports")
response = json_response(conn, 200)
notes = hd(response["reports"])["notes"]
[note, _] = notes
assert note["user"]["nickname"] == admin.nickname
assert note["content"] == "this is disgusting!"
assert note["created_at"]
assert response["total"] == 1
end
test "it deletes the note", %{conn: conn, report_id: report_id} do
assert ReportNote |> Repo.all() |> length() == 2
[note, _] = Repo.all(ReportNote)
delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
assert ReportNote |> Repo.all() |> length() == 1
end
end
test "GET /api/pleroma/admin/config/descriptions", %{conn: conn} do
admin = insert(:user, is_admin: true)
conn =
assign(conn, :user, admin)
|> get("/api/pleroma/admin/config/descriptions")
assert [child | _others] = json_response(conn, 200)
assert child["children"]
assert child["key"]
assert String.starts_with?(child["group"], ":")
assert child["description"]
end
end
# Needed for testing
defmodule Pleroma.Web.Endpoint.NotReal do
end
defmodule Pleroma.Captcha.NotReal do
end
diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs
index 810f371cb..fd2e40ce2 100644
--- a/test/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,1257 +1,1276 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
clear_config([:instance, :federating])
clear_config([:instance, :allow_relay])
describe "posting statuses" do
setup do: oauth_access(["write:statuses"])
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
Pleroma.Config.put([:instance, :federating], true)
Pleroma.Config.get([:instance, :allow_relay], true)
response =
conn
|> post("api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
"status" => "Hello world",
"visibility" => "public"
})
|> json_response(200)
assert response["reblogs_count"] == 0
ObanHelpers.perform_all()
response =
conn
|> get("api/v1/statuses/#{response["id"]}", %{})
|> json_response(200)
assert response["reblogs_count"] == 0
end
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
{:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
# Six hours
assert ttl > :timer.seconds(6 * 60 * 60 - 1)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 120 * 60
conn_four =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
assert activity = Activity.get_by_id(fourth_id)
assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
estimated_expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.truncate(:second)
# This assert will fail if the test takes longer than a minute. I sure hope it never does:
assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
assert fourth_response["pleroma"]["expires_at"] ==
NaiveDateTime.to_iso8601(expiration.scheduled_at)
end
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
# 1 hour
expires_in = 60 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response(422)
# 30 minutes
expires_in = 30 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response(422)
end
test "posting an undefined status with an attachment", %{user: user, conn: conn} do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
post(conn, "/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
assert json_response(conn, 200)
end
test "replying to a status", %{user: user, conn: conn} do
{:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
conn =
conn
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{
user: user,
conn: conn
} do
{:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
post(conn, "/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
real_status = json_response(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
post(conn, "/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
fake_status = json_response(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
assert real_status == fake_status
end
test "posting a status with OGP link preview", %{conn: conn} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
Config.put([:rich_media, :enabled], true)
conn =
post(conn, "/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
end
describe "posting scheduled statuses" do
setup do: oauth_access(["write:statuses"])
test "creates a scheduled activity", %{conn: conn} do
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
conn =
post(conn, "/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
post(conn, "/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
assert %{"type" => "image"} = media_attachment
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
conn =
post(conn, "/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
assert %{"content" => "not scheduled"} = json_response(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
end
test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
end
end
describe "posting polls" do
setup do: oauth_access(["write:statuses"])
test "posting a poll", %{conn: conn} do
time = NaiveDateTime.utc_now()
conn =
post(conn, "/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
})
response = json_response(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
refute response["poll"]["expred"]
question = Object.get_by_id(response["poll"]["id"])
# closed contains utc timezone
assert question.data["closed"] =~ "Z"
end
test "option limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
post(conn, "/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
post(conn, "/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
"expires_in" => 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
post(conn, "/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
post(conn, "/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Expiration date is too far in the future"
end
end
test "get a status" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id)
end
test "get a direct status" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
res = json_response(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs" do
%{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
end
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response(conn, 200)
refute Activity.get_by_id(activity.id)
end
test "when you didn't create it" do
%{conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity)
conn = delete(conn, "/api/v1/statuses/#{activity.id}")
assert %{"error" => _} = json_response(conn, 403)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're an admin or moderator", %{conn: conn} do
activity1 = insert(:note_activity)
activity2 = insert(:note_activity)
admin = insert(:user, is_admin: true)
moderator = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, admin)
|> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity1.id}")
assert %{} = json_response(res_conn, 200)
res_conn =
conn
|> assign(:user, moderator)
|> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity2.id}")
assert %{} = json_response(res_conn, 200)
refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id)
end
end
describe "reblogging" do
setup do: oauth_access(["write:statuses"])
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
CommonAPI.favorite(activity.id, user2)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
{:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
conn_res =
build_conn()
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
} = json_response(conn_res, 200)
conn_res =
build_conn()
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
} = json_response(conn_res, 200)
assert to_string(activity.id) == id
end
test "returns 400 error when activity is not exist", %{conn: conn} do
conn = post(conn, "/api/v1/statuses/foo/reblog")
assert json_response(conn, 400) == %{"error" => "Could not repeat"}
end
end
describe "unreblogging" do
setup do: oauth_access(["write:statuses"])
test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _, _} = CommonAPI.repeat(activity.id, user)
conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 400 error when activity is not exist", %{conn: conn} do
conn = post(conn, "/api/v1/statuses/foo/unreblog")
assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
end
end
describe "favoriting" do
setup do: oauth_access(["write:favourites"])
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "favoriting twice will just return 200", %{conn: conn} do
activity = insert(:note_activity)
post(conn, "/api/v1/statuses/#{activity.id}/favourite")
assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200)
end
test "returns 400 error for a wrong id", %{conn: conn} do
conn = post(conn, "/api/v1/statuses/1/favourite")
assert json_response(conn, 400) == %{"error" => "Could not favorite"}
end
end
describe "unfavoriting" do
setup do: oauth_access(["write:favourites"])
test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 400 error for a wrong id", %{conn: conn} do
conn = post(conn, "/api/v1/statuses/1/unfavourite")
assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
end
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
%{activity: activity}
end
clear_config([:instance, :max_pinned_statuses]) do
Config.put([:instance, :max_pinned_statuses], 1)
end
test "pin status", %{conn: conn, user: user, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => true} =
conn
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(200)
assert [%{"id" => ^id_str, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
assert json_response(conn, 400) == %{"error" => "Could not pin"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response(200)
assert [] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
conn = post(conn, "/api/v1/statuses/1/unpin")
assert json_response(conn, 400) == %{"error" => "Could not unpin"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response(400)
end
end
describe "cards" do
setup do
Config.put([:rich_media, :enabled], true)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
assert response_two == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(:ok)
assert response == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
}
}
end
end
test "bookmarks" do
%{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
author = insert(:user)
{:ok, activity1} =
CommonAPI.post(author, %{
"status" => "heweoo?"
})
{:ok, activity2} =
CommonAPI.post(author, %{
"status" => "heweoo!"
})
response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response(response1, 200)["bookmarked"] == true
response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response(response2, 200)["bookmarked"] == true
bookmarks = get(conn, "/api/v1/bookmarks")
assert [json_response(response2, 200), json_response(response1, 200)] ==
json_response(bookmarks, 200)
response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response(response1, 200)["bookmarked"] == false
bookmarks = get(conn, "/api/v1/bookmarks")
assert [json_response(response2, 200)] == json_response(bookmarks, 200)
end
describe "conversation muting" do
setup do: oauth_access(["write:mutes"])
setup do
post_user = insert(:user)
{:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
%{activity: activity}
end
test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => false} =
conn
# |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("api/v1/timelines/home")
[reblogged_activity] = json_response(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
%{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} wanna get some #cofe together?",
"visibility" => "direct"
})
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
build_conn()
|> get(favourited_by_url)
|> json_response(404)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
conn
|> assign(:token, nil)
|> get(favourited_by_url)
|> json_response(404)
response =
conn
|> get(favourited_by_url)
|> json_response(200)
[%{"id" => id}] = response
assert id == other_user.id
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
%{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
conn: conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} wanna get some #cofe together?",
"visibility" => "direct"
})
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(404)
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
response =
build_conn()
|> get("/api/v1/statuses/#{id3}/context")
|> json_response(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
first_conn = get(conn, "/api/v1/favourites")
assert [status] = json_response(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
"status" =>
"Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
last_like = status["id"]
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn = get(conn, "/api/v1/favourites?limit=0")
assert [] = json_response(third_conn, 200)
end
+
+ test "expires_at is nil for another user" do
+ %{conn: conn, user: user} = oauth_access(["read:statuses"])
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
+
+ expires_at =
+ activity.id
+ |> ActivityExpiration.get_by_activity_id()
+ |> Map.get(:scheduled_at)
+ |> NaiveDateTime.to_iso8601()
+
+ assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
+ conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
+
+ %{conn: conn} = oauth_access(["read:statuses"])
+
+ assert %{"pleroma" => %{"expires_at" => nil}} =
+ conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
+ end
end
diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs
index 751ca614c..f2ebbde7e 100644
--- a/test/web/rich_media/parsers/twitter_card_test.exs
+++ b/test/web/rich_media/parsers/twitter_card_test.exs
@@ -1,103 +1,113 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do
use ExUnit.Case, async: true
alias Pleroma.Web.RichMedia.Parsers.TwitterCard
test "returns error when html not contains twitter card" do
- assert TwitterCard.parse("", %{}) == {:error, "No twitter card metadata found"}
+ assert TwitterCard.parse([{"html", [], [{"head", [], []}, {"body", [], []}]}], %{}) ==
+ {:error, "No twitter card metadata found"}
end
test "parses twitter card with only name attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
%{
"app:id:googleplay": "com.nytimes.android",
"app:name:googleplay": "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622",
site: nil,
title:
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times"
}}
end
test "parses twitter card with only property attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
%{
card: "summary_large_image",
description:
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
image:
"https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
"image:alt": "",
title:
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
url:
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}}
end
test "parses twitter card with name & property attributes" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
%{
"app:id:googleplay": "com.nytimes.android",
"app:name:googleplay": "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622",
card: "summary_large_image",
description:
"With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.",
image:
"https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg",
"image:alt": "",
site: nil,
title:
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.",
url:
"https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html"
}}
end
test "respect only first title tag on the page" do
image_path =
"https://assets.atlasobscura.com/media/W1siZiIsInVwbG9hZHMvYXNzZXRzLzkwYzgyMzI4LThlMDUtNGRiNS05MDg3LTUzMGUxZTM5N2RmMmVkOTM5ZDM4MGM4OTIx" <>
"YTQ5MF9EQVIgZXhodW1hdGlvbiBvZiBNYXJnYXJldCBDb3JiaW4gZ3JhdmUgMTkyNi5qcGciXSxbInAiLCJjb252ZXJ0IiwiIl0sWyJwIiwiY29udmVydCIsIi1xdWFsaXR5IDgxIC1hdXRvLW9" <>
"yaWVudCJdLFsicCIsInRodW1iIiwiNjAweD4iXV0/DAR%20exhumation%20of%20Margaret%20Corbin%20grave%201926.jpg"
- html = File.read!("test/fixtures/margaret-corbin-grave-west-point.html")
+ html =
+ File.read!("test/fixtures/margaret-corbin-grave-west-point.html") |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
%{
site: "@atlasobscura",
title:
"The Missing Grave of Margaret Corbin, Revolutionary War Veteran - Atlas Obscura",
card: "summary_large_image",
image: image_path
}}
end
test "takes first founded title in html head if there is html markup error" do
- html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html")
+ html =
+ File.read!("test/fixtures/nypd-facial-recognition-children-teenagers4.html")
+ |> Floki.parse_document!()
assert TwitterCard.parse(html, %{}) ==
{:ok,
%{
site: nil,
title:
"She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times",
"app:id:googleplay": "com.nytimes.android",
"app:name:googleplay": "NYTimes",
"app:url:googleplay": "nytimes://reader/id/100000006583622"
}}
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Aug 9, 3:07 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724150
Default Alt Text
(314 KB)

Event Timeline