Merge remote-tracking branch 'pleroma/develop' into feature/addressable-lists

This commit is contained in:
Egor Kislitsyn 2019-05-17 19:57:14 +07:00
commit 557f0e33a7
55 changed files with 1724 additions and 214 deletions

View file

@ -10,18 +10,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc. - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics - [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction - Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database bump_all_conversations`
- Mix Tasks: `mix pleroma.database remove_embedded_objects` - Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Federation: Support for reports - Federation: Support for reports
- Configuration: `safe_dm_mentions` option - Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option - Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option - Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option - Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option - Configuration: Media proxy `whitelist` option
- Configuration: `report_uri` option
- Pleroma API: User subscriptions - Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint - Pleroma API: Healthcheck endpoint
- Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other - Admin API: Endpoints for making users follow/unfollow each other
- Admin API: added filters (role, tags, email, name) for users endpoint - Admin API: added filters (role, tags, email, name) for users endpoint
- Admin API: Endpoints for managing reports
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses
- AdminFE: initial release with basic user management accessible at /pleroma/admin/ - AdminFE: initial release with basic user management accessible at /pleroma/admin/
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
@ -99,7 +105,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`] - Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]
## Removed ## Removed
- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations` - Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`
## [0.9.9999] - 2019-04-05 ## [0.9.9999] - 2019-04-05
### Security ### Security

View file

@ -48,7 +48,8 @@
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes, types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter] telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil
config :pleroma, Pleroma.Captcha, config :pleroma, Pleroma.Captcha,
enabled: false, enabled: false,

View file

@ -61,6 +61,8 @@
config :pleroma, :app_account_creation, max_requests: 5 config :pleroma, :app_account_creation, max_requests: 5
config :pleroma, :http_security, report_uri: "https://endpoint.com"
try do try do
import_config "test.secret.exs" import_config "test.secret.exs"
rescue rescue

View file

@ -24,7 +24,7 @@ Authentication is required and the user must be an admin.
- 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` - 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: - Response:
```JSON ```json
{ {
"page_size": integer, "page_size": integer,
"count": integer, "count": integer,
@ -92,7 +92,7 @@ Authentication is required and the user must be an admin.
- `nickname` - `nickname`
- Response: Users object - Response: Users object
```JSON ```json
{ {
"deactivated": bool, "deactivated": bool,
"id": integer, "id": integer,
@ -124,7 +124,7 @@ Authentication is required and the user must be an admin.
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
@ -141,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
@ -223,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"invites": [ "invites": [
@ -250,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `token` - `token`
- Response: - Response:
```JSON ```json
{ {
"id": integer, "id": integer,
"token": string, "token": string,
@ -280,3 +280,280 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `GET` - Methods: `GET`
- Params: none - Params: none
- Response: password reset token (base64 string) - Response: password reset token (base64 string)
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
- Params:
- `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved`
- `limit`: optional, the number of records to retrieve
- `since_id`: optional, returns results that are more recent than the specified id
- `max_id`: optional, returns results that are older than the specified id
- 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
{
"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
},
"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
},
"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"
}
]
}
]
}
```
## `/api/pleroma/admin/reports/:id`
### Get an individual report
- Method `GET`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id`
### Change the state of the report
- Method `PUT`
- Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
- Response:
- On failure:
- 400 Bad Request `"Unsupported state"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report
- Method `POST`
- Params:
- `id`
- `status`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"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/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `/api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Method `PUT`
- 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
## `/api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Method `DELETE`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`

View file

@ -286,7 +286,8 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header * ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent * ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent * ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. * ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`
* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header.
## :mrf_user_allowlist ## :mrf_user_allowlist

View file

@ -1,30 +1,30 @@
# Introduction to Pleroma # Introduction to Pleroma
## What is Pleroma? ## What is Pleroma?
Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3. Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
One account on a instance is enough to talk to the entire fediverse! One account on a instance is enough to talk to the entire fediverse!
## How can I use it? ## How can I use it?
Pleroma instances are already widely deployed, a list can be found here: Pleroma instances are already widely deployed, a list can be found here:
http://distsn.org/pleroma-instances.html http://distsn.org/pleroma-instances.html
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
Installation instructions can be found here: Installation instructions can be found here:
[main Pleroma wiki](/) [main Pleroma wiki](/)
## I got an account, now what? ## I got an account, now what?
Great! Now you can explore the fediverse! Great! Now you can explore the fediverse!
- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password. - Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password.
(If you don't have one yet, click on Register) :slightly_smiling_face: (If you don't have one yet, click on Register) :slightly_smiling_face:
At this point you will have two columns in front of you. At this point you will have two columns in front of you.
### Left column ### Left column
- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers). - first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers).
Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000). Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000).
If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile: If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile:
To post your status, simply press Submit. To post your status, simply press Submit.
- second block: Here you can switch between the different timelines: - second block: Here you can switch between the different timelines:
@ -38,7 +38,7 @@ To post your status, simply press Submit.
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses. - fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses.
### Right column ### Right column
This is where the interesting stuff happens! :slight_smile: This is where the interesting stuff happens! :slight_smile:
Depending on the timeline you will see different statuses, but each status has a standard structure: Depending on the timeline you will see different statuses, but each status has a standard structure:
- Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). - Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status).
- A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime! - A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
@ -47,9 +47,9 @@ Depending on the timeline you will see different statuses, but each status has a
- Four buttons (left to right): Reply, Repeat, Favorite, Delete. - Four buttons (left to right): Reply, Repeat, Favorite, Delete.
## Mastodon interface ## Mastodon interface
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile: If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile:
Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks: Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks:
For more information on the Mastodon interface, please look here: For more information on the Mastodon interface, please look here:
https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.

View file

@ -4,6 +4,9 @@
defmodule Mix.Tasks.Pleroma.Database do defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common alias Mix.Tasks.Pleroma.Common
alias Pleroma.Conversation
alias Pleroma.Repo
alias Pleroma.User
require Logger require Logger
use Mix.Task use Mix.Task
@ -19,6 +22,14 @@ defmodule Mix.Tasks.Pleroma.Database do
Options: Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
## Create a conversation for all existing DMs. Can be safely re-run.
mix pleroma.database bump_all_conversations
## Remove duplicated items from following and update followers count for all users
mix pleroma.database update_users_following_followers_counts
""" """
def run(["remove_embedded_objects" | args]) do def run(["remove_embedded_objects" | args]) do
{options, [], []} = {options, [], []} =
@ -32,7 +43,7 @@ def run(["remove_embedded_objects" | args]) do
Common.start_pleroma() Common.start_pleroma()
Logger.info("Removing embedded objects") Logger.info("Removing embedded objects")
Pleroma.Repo.query!( Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[], [],
timeout: :infinity timeout: :infinity
@ -41,11 +52,24 @@ def run(["remove_embedded_objects" | args]) do
if Keyword.get(options, :vacuum) do if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL") Logger.info("Runnning VACUUM FULL")
Pleroma.Repo.query!( Repo.query!(
"vacuum full;", "vacuum full;",
[], [],
timeout: :infinity timeout: :infinity
) )
end end
end end
def run(["bump_all_conversations"]) do
Common.start_pleroma()
Conversation.bump_for_all_activities()
end
def run(["update_users_following_followers_counts"]) do
Common.start_pleroma()
users = Repo.all(User)
Enum.each(users, &User.remove_duplicated_following/1)
Enum.each(users, &User.update_follower_count/1)
end
end end

View file

@ -77,6 +77,10 @@ defmodule Mix.Tasks.Pleroma.User do
## Delete tags from a user. ## Delete tags from a user.
mix pleroma.user untag NICKNAME TAGS mix pleroma.user untag NICKNAME TAGS
## Toggle confirmation of the user's account.
mix pleroma.user toggle_confirmed NICKNAME
""" """
def run(["new", nickname, email | rest]) do def run(["new", nickname, email | rest]) do
{options, [], []} = {options, [], []} =
@ -388,6 +392,21 @@ def run(["delete_activities", nickname]) do
end end
end end
def run(["toggle_confirmed", nickname]) do
Common.start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.toggle_confirmation(user)
message = if user.info.confirmation_pending, do: "needs", else: "doesn't need"
Mix.shell().info("#{nickname} #{message} confirmation.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
end
end
defp set_moderator(user, value) do defp set_moderator(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})

View file

@ -60,21 +60,24 @@ defmodule Pleroma.Activity do
timestamps() timestamps()
end end
def with_preloaded_object(query) do def with_joined_object(query) do
query join(query, :inner, [activity], o in Object,
|> join(
:inner,
[activity],
o in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data, o.data,
activity.data, activity.data,
activity.data activity.data
) ),
as: :object
) )
|> preload([activity, object], object: object) end
def with_preloaded_object(query) do
query
|> has_named_binding?(:object)
|> if(do: query, else: with_joined_object(query))
|> preload([activity, object: object], object: object)
end end
def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, %User{} = user) do
@ -108,7 +111,7 @@ def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do def change(struct, params \\ %{}) do
struct struct
|> cast(params, [:data]) |> cast(params, [:data, :recipients])
|> validate_required([:data]) |> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index) |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end end

View file

@ -95,7 +95,6 @@ def handle_command(state, "home") do
activities = activities =
[user.ap_id | user.following] [user.ap_id | user.following]
|> ActivityPub.fetch_activities(params) |> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
Enum.each(activities, fn activity -> Enum.each(activities, fn activity ->
puts_activity(activity) puts_activity(activity)

View file

@ -45,7 +45,7 @@ def get_for_ap_id(ap_id) do
2. Create a participation for all the people involved who don't have one already 2. Create a participation for all the people involved who don't have one already
3. Bump all relevant participations to 'unread' 3. Bump all relevant participations to 'unread'
""" """
def create_or_bump_for(activity) do def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"], "Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity), object <- Pleroma.Object.normalize(activity),
@ -58,7 +58,7 @@ def create_or_bump_for(activity) do
participations = participations =
Enum.map(users, fn user -> Enum.map(users, fn user ->
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation) Participation.create_for_user_and_conversation(user, conversation, opts)
participation participation
end) end)
@ -72,4 +72,21 @@ def create_or_bump_for(activity) do
e -> {:error, e} e -> {:error, e}
end end
end end
@doc """
This is only meant to be run by a mix task. It creates conversations/participations for all direct messages in the database.
"""
def bump_for_all_activities do
stream =
Pleroma.Web.ActivityPub.ActivityPub.fetch_direct_messages_query()
|> Repo.stream()
Repo.transaction(
fn ->
stream
|> Enum.each(fn a -> create_or_bump_for(a, read: true) end)
end,
timeout: :infinity
)
end
end end

View file

@ -22,15 +22,17 @@ defmodule Pleroma.Conversation.Participation do
def creation_cng(struct, params) do def creation_cng(struct, params) do
struct struct
|> cast(params, [:user_id, :conversation_id]) |> cast(params, [:user_id, :conversation_id, :read])
|> validate_required([:user_id, :conversation_id]) |> validate_required([:user_id, :conversation_id])
end end
def create_for_user_and_conversation(user, conversation) do def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read]
%__MODULE__{} %__MODULE__{}
|> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read})
|> Repo.insert( |> Repo.insert(
on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
returning: true, returning: true,
conflict_target: [:user_id, :conversation_id] conflict_target: [:user_id, :conversation_id]
) )

View file

@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do
end end
statuses_html = statuses_html =
if length(statuses) > 0 do if is_list(statuses) && length(statuses) > 0 do
statuses_list_html = statuses_list_html =
statuses statuses
|> Enum.map(fn |> Enum.map(fn

View file

@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do
query = query =
from( from(
f in Pleroma.Filter, f in Pleroma.Filter,
where: f.user_id == ^user_id where: f.user_id == ^user_id,
order_by: [desc: :id]
) )
Repo.all(query) Repo.all(query)

View file

@ -20,8 +20,9 @@ def call(conn, _options) do
defp headers do defp headers do
referrer_policy = Config.get([:http_security, :referrer_policy]) referrer_policy = Config.get([:http_security, :referrer_policy])
report_uri = Config.get([:http_security, :report_uri])
[ headers = [
{"x-xss-protection", "1; mode=block"}, {"x-xss-protection", "1; mode=block"},
{"x-permitted-cross-domain-policies", "none"}, {"x-permitted-cross-domain-policies", "none"},
{"x-frame-options", "DENY"}, {"x-frame-options", "DENY"},
@ -30,12 +31,27 @@ defp headers do
{"x-download-options", "noopen"}, {"x-download-options", "noopen"},
{"content-security-policy", csp_string() <> ";"} {"content-security-policy", csp_string() <> ";"}
] ]
if report_uri do
report_group = %{
"group" => "csp-endpoint",
"max-age" => 10_886_400,
"endpoints" => [
%{"url" => report_uri}
]
}
headers ++ [{"reply-to", Jason.encode!(report_group)}]
else
headers
end
end end
defp csp_string do defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url() static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = Pleroma.Web.Endpoint.websocket_url() websocket_url = Pleroma.Web.Endpoint.websocket_url()
report_uri = Config.get([:http_security, :report_uri])
connect_src = "connect-src 'self' #{static_url} #{websocket_url}" connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
@ -53,7 +69,7 @@ defp csp_string do
"script-src 'self'" "script-src 'self'"
end end
[ main_part = [
"default-src 'none'", "default-src 'none'",
"base-uri 'self'", "base-uri 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
@ -63,11 +79,14 @@ defp csp_string do
"font-src 'self'", "font-src 'self'",
"manifest-src 'self'", "manifest-src 'self'",
connect_src, connect_src,
script_src, script_src
if scheme == "https" do
"upgrade-insecure-requests"
end
] ]
report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
(main_part ++ report ++ insecure)
|> Enum.join("; ") |> Enum.join("; ")
end end

View file

@ -55,7 +55,7 @@ defmodule Pleroma.User do
field(:last_refreshed_at, :naive_datetime_usec) field(:last_refreshed_at, :naive_datetime_usec)
has_many(:notifications, Notification) has_many(:notifications, Notification)
has_many(:registrations, Registration) has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info) embeds_one(:info, User.Info)
timestamps() timestamps()
end end
@ -166,7 +166,7 @@ def remote_user_creation(params) do
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:bio, :name, :avatar]) |> cast(params, [:bio, :name, :avatar, :following])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
@ -233,7 +233,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> unique_constraint(:email) |> unique_constraint(:email)
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000) |> validate_length(:bio, max: 1000)
@ -278,7 +278,7 @@ def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset), with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user), {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user), {:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user} {:ok, user}
end end
@ -709,6 +709,18 @@ def update_follower_count(%User{} = user) do
end end
end end
def remove_duplicated_following(%User{following: following} = user) do
uniq_following = Enum.uniq(following)
if length(following) == length(uniq_following) do
{:ok, user}
else
user
|> update_changeset(%{following: uniq_following})
|> update_and_set_cache()
end
end
@spec get_users_from_set([String.t()], boolean()) :: [User.t()] @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do def get_users_from_set(ap_ids, local_only \\ true) do
criteria = %{ap_id: ap_ids, deactivated: false} criteria = %{ap_id: ap_ids, deactivated: false}
@ -1132,7 +1144,6 @@ def delete_user_activities(%User{ap_id: ap_id} = user) do
stream = stream =
ap_id ap_id
|> Activity.query_by_actor() |> Activity.query_by_actor()
|> Activity.with_preloaded_object()
|> Repo.stream() |> Repo.stream()
Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
@ -1378,4 +1389,17 @@ def all_superusers do
def showing_reblogs?(%User{} = user, %User{} = target) do def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs target.ap_id not in user.info.muted_reblogs
end end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending
info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
user
|> change()
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end
end end

View file

@ -212,7 +212,7 @@ def profile_update(info, params) do
]) ])
end end
@spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t() @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
def confirmation_changeset(info, opts) do def confirmation_changeset(info, opts) do
need_confirmation? = Keyword.get(opts, :need_confirmation) need_confirmation? = Keyword.get(opts, :need_confirmation)

View file

@ -539,8 +539,6 @@ defp restrict_visibility(query, %{visibility: visibility})
) )
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
else else
Logger.error("Could not restrict visibility to #{visibility}") Logger.error("Could not restrict visibility to #{visibility}")
@ -556,8 +554,6 @@ defp restrict_visibility(query, %{visibility: visibility})
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
end end
@ -568,6 +564,18 @@ defp restrict_visibility(_query, %{visibility: visibility})
defp restrict_visibility(query, _visibility), do: query defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
query =
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
query
end
defp restrict_thread_visibility(query, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do def fetch_user_activities(user, reading_user, params \\ %{}) do
params = params =
params params
@ -694,6 +702,12 @@ defp restrict_type(query, %{"type" => type}) do
defp restrict_type(query, _), do: query defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from( from(
activity in query, activity in query,
@ -749,8 +763,11 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || [] blocks = info.blocks || []
domain_blocks = info.domain_blocks || [] domain_blocks = info.domain_blocks || []
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from( from(
activity in query, [activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks), where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks), where: fragment("not (? && ?)", activity.recipients, ^blocks),
where: where:
@ -760,7 +777,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
activity.data, activity.data,
^blocks ^blocks
), ),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks) where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
) )
end end
@ -840,11 +858,13 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_local(opts) |> restrict_local(opts)
|> restrict_actor(opts) |> restrict_actor(opts)
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_muted(opts) |> restrict_muted(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts)
|> restrict_replies(opts) |> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
@ -983,11 +1003,10 @@ def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user) contain_broken_threads(activity, user)
end end
# do post-processing on a timeline def fetch_direct_messages_query do
def contain_timeline(timeline, user) do Activity
timeline |> restrict_type(%{"type" => "Create"})
|> Enum.filter(fn activity -> |> restrict_visibility(%{visibility: "direct"})
contain_activity(activity, user) |> order_by([activity], asc: activity.id)
end)
end end
end end

View file

@ -55,7 +55,7 @@ defp check_media_nsfw(
object = object =
if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
tags = (child_object["tag"] || []) ++ ["nsfw"] tags = (child_object["tag"] || []) ++ ["nsfw"]
child_object = Map.put(child_object, "tags", tags) child_object = Map.put(child_object, "tag", tags)
child_object = Map.put(child_object, "sensitive", true) child_object = Map.put(child_object, "sensitive", true)
Map.put(object, "object", child_object) Map.put(object, "object", child_object)
else else

View file

@ -31,7 +31,7 @@ defp process_tag(
object = object =
object object
|> Map.put("tags", tags) |> Map.put("tag", tags)
|> Map.put("sensitive", true) |> Map.put("sensitive", true)
message = Map.put(message, "object", object) message = Map.put(message, "object", object)

View file

@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility

View file

@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger require Logger
@supported_object_types ["Article", "Note", "Video", "Page"] @supported_object_types ["Article", "Note", "Video", "Page"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
@ -670,7 +672,8 @@ def make_flag_data(params, additional) do
"actor" => params.actor.ap_id, "actor" => params.actor.ap_id,
"content" => params.content, "content" => params.content,
"object" => object, "object" => object,
"context" => params.context "context" => params.context,
"state" => "open"
} }
|> Map.merge(additional) |> Map.merge(additional)
end end
@ -713,4 +716,77 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
end end
end end
end end
#### Report-related helpers
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
with new_data <- Map.put(activity.data, "state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = "https://www.w3.org/ns/activitystreams#Public"
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
end end

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.ActivityPub.Visibility do defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
@ -13,11 +14,12 @@ def is_public?(data) do
end end
def is_private?(activity) do def is_private?(activity) do
unless is_public?(activity) do with false <- is_public?(activity),
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address %User{follower_address: follower_address} <-
Enum.any?(activity.data["to"], &(&1 == follower_address)) User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
else else
false _ -> false
end end
end end
@ -38,25 +40,14 @@ def visible_for_user?(activity, user) do
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end end
# guard def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
def entire_thread_visible_for_user?(nil, _user), do: false {:ok, %{rows: [[result]]}} =
Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [
user.ap_id,
activity.data["id"]
])
# XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop result
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
def entire_thread_visible_for_user?(
%Activity{} = tail,
# %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
) do
case Object.normalize(tail) do
%{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
_ ->
visible_for_user?(tail, user)
end
end end
def get_visibility(object) do def get_visibility(object) do

View file

@ -4,11 +4,16 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -287,12 +292,88 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token) |> json(token.token)
end end
def list_reports(conn, params) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
reports =
[]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
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: report})
else
_ -> {:error, :not_found}
end
end
def report_update_state(conn, %{"id" => id, "state" => state}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
end
end
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
with false <- is_nil(params["status"]),
%Activity{} <- Activity.get_by_id(id) do
params =
params
|> Map.put("in_reply_to_status_id", id)
|> Map.put("visibility", "direct")
{:ok, activity} = CommonAPI.post(user, params)
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
else
true ->
{:param_cast, nil}
nil ->
{:error, :not_found}
end
end
def status_update(conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
end
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(404) |> put_status(404)
|> json("Not found") |> json("Not found")
end end
def errors(conn, {:error, reason}) do
conn
|> put_status(400)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do def errors(conn, {:param_cast, _}) do
conn conn
|> put_status(400) |> put_status(400)

View file

@ -0,0 +1,41 @@
# 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.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do
%{
reports: render_many(reports, __MODULE__, "show.json", as: :report)
}
end
def render("show.json", %{report: report}) do
user = User.get_cached_by_ap_id(report.data["actor"])
created_at = Utils.to_masto_date(report.data["published"])
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{
id: report.id,
account: AccountView.render("account.json", %{user: account}),
actor: AccountView.render("account.json", %{user: user}),
content: report.data["content"],
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"]
}
end
end

View file

@ -71,6 +71,9 @@ def delete(activity_id, user) do
{:ok, _} <- unpin(activity_id, user), {:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete} {:ok, delete}
else
_ ->
{:error, "Could not delete"}
end end
end end
@ -318,6 +321,60 @@ def report(user, data) do
end end
end end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id),
{:ok, activity} <- Utils.update_report_state(activity, state) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
_ ->
{:error, "Could not update state"}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts),
{:ok, activity} <- set_visibility(activity, opts) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{"visibility" => visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, muted) do def hide_reblogs(user, muted) do
ap_id = muted.ap_id ap_id = muted.ap_id

View file

@ -246,13 +246,11 @@ def make_note_data(
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
} }
if in_reply_to do with false <- is_nil(in_reply_to),
in_reply_to_object = Object.normalize(in_reply_to) %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
object
|> Map.put("inReplyTo", in_reply_to_object.data["id"])
else else
object _ -> object
end end
end end

View file

@ -52,9 +52,9 @@ def perform(type, _, _) do
@doc """ @doc """
Relays an activity to all specified peers. Relays an activity to all specified peers.
""" """
@callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()} @callback publish(User.t(), Activity.t()) :: :ok | {:error, any()}
@spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok @spec publish(User.t(), Activity.t()) :: :ok
def publish(%User{} = user, %Activity{} = activity) do def publish(%User{} = user, %Activity{} = activity) do
Config.get([:instance, :federation_publisher_modules]) Config.get([:instance, :federation_publisher_modules])
|> Enum.each(fn module -> |> Enum.each(fn module ->
@ -70,9 +70,9 @@ def publish(%User{} = user, %Activity{} = activity) do
@doc """ @doc """
Gathers links used by an outgoing federation module for WebFinger output. Gathers links used by an outgoing federation module for WebFinger output.
""" """
@callback gather_webfinger_links(Pleroma.User.t()) :: list() @callback gather_webfinger_links(User.t()) :: list()
@spec gather_webfinger_links(Pleroma.User.t()) :: list() @spec gather_webfinger_links(User.t()) :: list()
def gather_webfinger_links(%User{} = user) do def gather_webfinger_links(%User{} = user) do
Config.get([:instance, :federation_publisher_modules]) Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links -> |> Enum.reduce([], fn module, links ->

View file

@ -303,7 +303,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
activities = activities =
[user.ap_id | user.following] [user.ap_id | user.following]
|> ActivityPub.fetch_activities(params) |> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse() |> Enum.reverse()
conn conn
@ -1223,7 +1222,7 @@ def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_id
accounts accounts
|> Enum.each(fn account_id -> |> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed) Pleroma.List.unfollow(list, followed)
end end
end) end)

View file

@ -40,7 +40,7 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target) follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested = requested =
if follow_activity do if follow_activity && !User.following?(target, user) do
follow_activity.data["state"] == "pending" follow_activity.data["state"] == "pending"
else else
false false

View file

@ -20,7 +20,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec) field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false) field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:app, App) belongs_to(:app, App)
timestamps() timestamps()

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:refresh_token, :string) field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec) field(:valid_until, :naive_datetime_usec)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId) belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:app, App) belongs_to(:app, App)
timestamps() timestamps()

View file

@ -194,6 +194,14 @@ defmodule Pleroma.Web.Router do
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)
get("/reports", AdminAPIController, :list_reports)
get("/reports/:id", AdminAPIController, :report_show)
put("/reports/:id", AdminAPIController, :report_update_state)
post("/reports/:id/respond", AdminAPIController, :report_respond)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
end end
scope "/", Pleroma.Web.TwitterAPI do scope "/", Pleroma.Web.TwitterAPI do

View file

@ -101,9 +101,7 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("user", user) |> Map.put("user", user)
activities = activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
conn conn
|> put_view(ActivityView) |> put_view(ActivityView)

View file

@ -99,7 +99,7 @@ def ensure_keys_present(user) do
info_cng = info_cng =
info info
|> Pleroma.User.Info.set_keys(pem) |> User.Info.set_keys(pem)
cng = cng =
Ecto.Changeset.change(user) Ecto.Changeset.change(user)

View file

@ -66,7 +66,10 @@ defp deps do
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"}, {:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"}, {:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~>3.0.5"}, {:ecto_sql,
git: "https://github.com/elixir-ecto/ecto_sql",
ref: "14cb065a74c488d737d973f7a91bc036c6245f78",
override: true},
{:postgrex, ">= 0.13.5"}, {:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.15"}, {:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"}, {:comeonin, "~> 4.1.1"},

View file

@ -16,12 +16,12 @@
"cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"db_connection": {:hex, :db_connection, "2.0.5", "ddb2ba6761a08b2bb9ca0e7d260e8f4dd39067426d835c24491a321b7f92a4da", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"},
"decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ecto": {:hex, :ecto, "3.1.4", "69d852da7a9f04ede725855a35ede48d158ca11a404fe94f8b2fb3b2162cd3c9", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "ecto_sql": {:git, "https://github.com/elixir-ecto/ecto_sql", "14cb065a74c488d737d973f7a91bc036c6245f78", [ref: "14cb065a74c488d737d973f7a91bc036c6245f78"]},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@ -66,7 +66,7 @@
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "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"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [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"}, "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [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"},
"prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"}, "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [: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"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [: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"},
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
@ -79,11 +79,11 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [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]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [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]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
"telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, 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]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, 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]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [: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", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [: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", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"}, "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},

View file

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetDefaultStateToReports do
use Ecto.Migration
def up do
execute """
UPDATE activities AS a
SET data = jsonb_set(data, '{state}', '"open"', true)
WHERE data->>'type' = 'Flag'
"""
end
def down do
execute """
UPDATE activities AS a
SET data = data #- '{state}'
WHERE data->>'type' = 'Flag'
"""
end
end

View file

@ -0,0 +1,73 @@
defmodule Pleroma.Repo.Migrations.AddThreadVisibilityFunction do
use Ecto.Migration
@disable_ddl_transaction true
def up do
statement = """
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$
DECLARE
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
child objects%ROWTYPE;
activity activities%ROWTYPE;
actor_user users%ROWTYPE;
author_fa varchar;
valid_recipients varchar[];
BEGIN
--- Fetch our actor.
SELECT * INTO actor_user FROM users WHERE users.ap_id = actor;
--- Fetch our initial activity.
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
LOOP
--- Ensure that we have an activity before continuing.
--- If we don't, the thread is not satisfiable.
IF activity IS NULL THEN
RETURN false;
END IF;
--- We only care about Create activities.
IF activity.data->>'type' != 'Create' THEN
RETURN true;
END IF;
--- Normalize the child object into child.
SELECT * INTO child FROM objects
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
--- Fetch the author's AS2 following collection.
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
--- Prepare valid recipients array.
valid_recipients := ARRAY[actor, public];
IF ARRAY[author_fa] && actor_user.following THEN
valid_recipients := valid_recipients || author_fa;
END IF;
--- Check visibility.
IF NOT valid_recipients && activity.recipients THEN
--- activity not visible, break out of the loop
RETURN false;
END IF;
--- If there's a parent, load it and do this all over again.
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
SELECT * INTO activity FROM activities
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE child.data->>'inReplyTo' = objects.data->>'id';
ELSE
RETURN true;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
"""
execute(statement)
end
def down do
execute("drop function thread_visibility(actor varchar, activity_id varchar)")
end
end

View file

@ -11,6 +11,26 @@ defmodule Pleroma.ConversationTest do
import Pleroma.Factory import Pleroma.Factory
test "it goes through old direct conversations" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"visibility" => "direct", "status" => "hey @#{other_user.nickname}"})
Repo.delete_all(Conversation)
Repo.delete_all(Conversation.Participation)
refute Repo.one(Conversation)
Conversation.bump_for_all_activities()
assert Repo.one(Conversation)
[participation, _p2] = Repo.all(Conversation.Participation)
assert participation.read
end
test "it creates a conversation for given ap_id" do test "it creates a conversation for given ap_id" do
assert {:ok, %Conversation{} = conversation} = assert {:ok, %Conversation{} = conversation} =
Conversation.create_for_ap_id("https://some_ap_id") Conversation.create_for_ap_id("https://some_ap_id")

View file

@ -125,7 +125,7 @@ test "gives a replacement for user links, using local nicknames in user links te
archaeme = archaeme =
insert(:user, %{ insert(:user, %{
nickname: "archa_eme_", nickname: "archa_eme_",
info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}} info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}}
}) })
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})

View file

@ -7,28 +7,89 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
alias Pleroma.Config alias Pleroma.Config
alias Plug.Conn alias Plug.Conn
test "it sends CSP headers when enabled", %{conn: conn} do describe "http security enabled" do
Config.put([:http_security, :enabled], true) setup do
enabled = Config.get([:http_securiy, :enabled])
conn = Config.put([:http_security, :enabled], true)
conn
|> get("/api/v1/instance")
refute Conn.get_resp_header(conn, "x-xss-protection") == [] on_exit(fn ->
refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] Config.put([:http_security, :enabled], enabled)
refute Conn.get_resp_header(conn, "x-frame-options") == [] end)
refute Conn.get_resp_header(conn, "x-content-type-options") == []
refute Conn.get_resp_header(conn, "x-download-options") == [] :ok
refute Conn.get_resp_header(conn, "referrer-policy") == [] end
refute Conn.get_resp_header(conn, "content-security-policy") == []
test "it sends CSP headers when enabled", %{conn: conn} do
conn = get(conn, "/api/v1/instance")
refute Conn.get_resp_header(conn, "x-xss-protection") == []
refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
refute Conn.get_resp_header(conn, "x-frame-options") == []
refute Conn.get_resp_header(conn, "x-content-type-options") == []
refute Conn.get_resp_header(conn, "x-download-options") == []
refute Conn.get_resp_header(conn, "referrer-policy") == []
refute Conn.get_resp_header(conn, "content-security-policy") == []
end
test "it sends STS headers when enabled", %{conn: conn} do
Config.put([:http_security, :sts], true)
conn = get(conn, "/api/v1/instance")
refute Conn.get_resp_header(conn, "strict-transport-security") == []
refute Conn.get_resp_header(conn, "expect-ct") == []
end
test "it does not send STS headers when disabled", %{conn: conn} do
Config.put([:http_security, :sts], false)
conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "strict-transport-security") == []
assert Conn.get_resp_header(conn, "expect-ct") == []
end
test "referrer-policy header reflects configured value", %{conn: conn} do
conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"]
Config.put([:http_security, :referrer_policy], "no-referrer")
conn =
build_conn()
|> get("/api/v1/instance")
assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"]
end
test "it sends `report-to` & `report-uri` CSP response headers" do
conn =
build_conn()
|> get("/api/v1/instance")
[csp] = Conn.get_resp_header(conn, "content-security-policy")
assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;|
[reply_to] = Conn.get_resp_header(conn, "reply-to")
assert reply_to ==
"{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}"
end
end end
test "it does not send CSP headers when disabled", %{conn: conn} do test "it does not send CSP headers when disabled", %{conn: conn} do
enabled = Config.get([:http_securiy, :enabled])
Config.put([:http_security, :enabled], false) Config.put([:http_security, :enabled], false)
conn = on_exit(fn ->
conn Config.put([:http_security, :enabled], enabled)
|> get("/api/v1/instance") end)
conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "x-xss-protection") == [] assert Conn.get_resp_header(conn, "x-xss-protection") == []
assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
@ -38,46 +99,4 @@ test "it does not send CSP headers when disabled", %{conn: conn} do
assert Conn.get_resp_header(conn, "referrer-policy") == [] assert Conn.get_resp_header(conn, "referrer-policy") == []
assert Conn.get_resp_header(conn, "content-security-policy") == [] assert Conn.get_resp_header(conn, "content-security-policy") == []
end end
test "it sends STS headers when enabled", %{conn: conn} do
Config.put([:http_security, :enabled], true)
Config.put([:http_security, :sts], true)
conn =
conn
|> get("/api/v1/instance")
refute Conn.get_resp_header(conn, "strict-transport-security") == []
refute Conn.get_resp_header(conn, "expect-ct") == []
end
test "it does not send STS headers when disabled", %{conn: conn} do
Config.put([:http_security, :enabled], true)
Config.put([:http_security, :sts], false)
conn =
conn
|> get("/api/v1/instance")
assert Conn.get_resp_header(conn, "strict-transport-security") == []
assert Conn.get_resp_header(conn, "expect-ct") == []
end
test "referrer-policy header reflects configured value", %{conn: conn} do
Config.put([:http_security, :enabled], true)
conn =
conn
|> get("/api/v1/instance")
assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"]
Config.put([:http_security, :referrer_policy], "no-referrer")
conn =
build_conn()
|> get("/api/v1/instance")
assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"]
end
end end

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase
alias Pleroma.Plugs.LegacyAuthenticationPlug alias Pleroma.Plugs.LegacyAuthenticationPlug
alias Pleroma.User alias Pleroma.User

View file

@ -1,23 +1,24 @@
defmodule Pleroma.RepoTest do defmodule Pleroma.RepoTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.User
describe "find_resource/1" do describe "find_resource/1" do
test "returns user" do test "returns user" do
user = insert(:user) user = insert(:user)
query = from(t in Pleroma.User, where: t.id == ^user.id) query = from(t in User, where: t.id == ^user.id)
assert Repo.find_resource(query) == {:ok, user} assert Repo.find_resource(query) == {:ok, user}
end end
test "returns not_found" do test "returns not_found" do
query = from(t in Pleroma.User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw")
assert Repo.find_resource(query) == {:error, :not_found} assert Repo.find_resource(query) == {:error, :not_found}
end end
end end
describe "get_assoc/2" do describe "get_assoc/2" do
test "get assoc from preloaded data" do test "get assoc from preloaded data" do
user = %Pleroma.User{name: "Agent Smith"} user = %User{name: "Agent Smith"}
token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user}
assert Repo.get_assoc(token, :user) == {:ok, user} assert Repo.get_assoc(token, :user) == {:ok, user}
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Factory do defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo use ExMachina.Ecto, repo: Pleroma.Repo
alias Pleroma.User
def participation_factory do def participation_factory do
conversation = insert(:conversation) conversation = insert(:conversation)
@ -23,7 +24,7 @@ def conversation_factory do
end end
def user_factory do def user_factory do
user = %Pleroma.User{ user = %User{
name: sequence(:name, &"Test テスト User #{&1}"), name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"), email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"), nickname: sequence(:nickname, &"nick#{&1}"),
@ -34,16 +35,16 @@ def user_factory do
%{ %{
user user
| ap_id: Pleroma.User.ap_id(user), | ap_id: User.ap_id(user),
follower_address: Pleroma.User.ap_followers(user), follower_address: User.ap_followers(user),
following: [Pleroma.User.ap_id(user)] following: [User.ap_id(user)]
} }
end end
def note_factory(attrs \\ %{}) do def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}") text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = insert(:user) user = attrs[:user] || insert(:user)
data = %{ data = %{
"type" => "Note", "type" => "Note",
@ -113,7 +114,8 @@ def direct_note_activity_factory do
end end
def note_activity_factory(attrs \\ %{}) do def note_activity_factory(attrs \\ %{}) do
note = attrs[:note] || insert(:note) user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data = %{ data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.DatabaseTest do
alias Pleroma.Repo
alias Pleroma.User
use Pleroma.DataCase
import Pleroma.Factory
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do
[user, user2] = insert_pair(:user)
{:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
assert length(following) == 2
assert info.follower_count == 0
info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
{:ok, user} =
user
|> Ecto.Changeset.change(%{following: following ++ following})
|> Ecto.Changeset.put_embed(:info, info_cng)
|> Repo.update()
assert length(user.following) == 4
assert user.info.follower_count == 3
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
user = User.get_by_id(user.id)
assert length(user.following) == 2
assert user.info.follower_count == 0
end
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
use Pleroma.DataCase use Pleroma.DataCase
@ -338,4 +339,31 @@ test "activities are deleted" do
assert message == "User #{nickname} statuses deleted." assert message == "User #{nickname} statuses deleted."
end end
end end
describe "running toggle_confirmed" do
test "user is confirmed" do
%{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false})
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} needs confirmation."
user = Repo.get(User, id)
assert user.info.confirmation_pending
assert user.info.confirmation_token
end
test "user is not confirmed" do
%{id: id, nickname: nickname} =
insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} doesn't need confirmation."
user = Repo.get(User, id)
refute user.info.confirmation_pending
refute user.info.confirmation_token
end
end
end end

View file

@ -277,7 +277,7 @@ test "it requires an email, name, nickname and password, bio is optional" do
end end
test "it restricts certain nicknames" do test "it restricts certain nicknames" do
[restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_bitstring(restricted_name) assert is_bitstring(restricted_name)
@ -626,6 +626,37 @@ test "it sets the info->follower_count property" do
end end
end end
describe "remove duplicates from following list" do
test "it removes duplicates" do
user = insert(:user)
follower = insert(:user)
{:ok, %User{following: following} = follower} = User.follow(follower, user)
assert length(following) == 2
{:ok, follower} =
follower
|> User.update_changeset(%{following: following ++ following})
|> Repo.update()
assert length(follower.following) == 4
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
test "it does nothing when following is uniq" do
user = insert(:user)
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
assert length(follower.following) == 2
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
end
describe "follow_import" do describe "follow_import" do
test "it imports user followings from list" do test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user) [user1, user2, user3] = insert_list(3, :user)
@ -873,7 +904,6 @@ test "hide a user's statuses from timelines and notifications" do
assert [activity] == assert [activity] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
|> ActivityPub.contain_timeline(user2)
{:ok, _user} = User.deactivate(user) {:ok, _user} = User.deactivate(user)
@ -882,7 +912,6 @@ test "hide a user's statuses from timelines and notifications" do
assert [] == assert [] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
|> ActivityPub.contain_timeline(user2)
end end
end end
@ -1194,14 +1223,32 @@ test "follower count is updated when a follower is blocked" do
follower2 = insert(:user) follower2 = insert(:user)
follower3 = insert(:user) follower3 = insert(:user)
{:ok, follower} = Pleroma.User.follow(follower, user) {:ok, follower} = User.follow(follower, user)
{:ok, _follower2} = Pleroma.User.follow(follower2, user) {:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = Pleroma.User.follow(follower3, user) {:ok, _follower3} = User.follow(follower3, user)
{:ok, _} = Pleroma.User.block(user, follower) {:ok, _} = User.block(user, follower)
user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user}) user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user})
assert Map.get(user_show, "followers_count") == 2 assert Map.get(user_show, "followers_count") == 2
end end
describe "toggle_confirmation/1" do
test "if user is confirmed" do
user = insert(:user, info: %{confirmation_pending: false})
{:ok, user} = User.toggle_confirmation(user)
assert user.info.confirmation_pending
assert user.info.confirmation_token
end
test "if user is unconfirmed" do
user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
{:ok, user} = User.toggle_confirmation(user)
refute user.info.confirmation_pending
refute user.info.confirmation_token
end
end
end end

View file

@ -462,6 +462,29 @@ test "doesn't return announce activities concerning blocked users" do
refute Enum.member?(activities, activity_three.id) refute Enum.member?(activities, activity_three.id)
end end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do test "doesn't return muted activities" do
activity_one = insert(:note_activity) activity_one = insert(:note_activity)
activity_two = insert(:note_activity) activity_two = insert(:note_activity)
@ -960,17 +983,21 @@ test "it filters broken threads" do
"in_reply_to_status_id" => private_activity_2.id "in_reply_to_status_id" => private_activity_2.id
}) })
activities = ActivityPub.fetch_activities([user1.ap_id | user1.following]) activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity, private_activity_1, private_activity_3] == activities assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
assert length(activities) == 3 assert length(activities) == 3
activities = ActivityPub.contain_timeline(activities, user1) activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity, private_activity_1] == activities assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2 assert length(activities) == 2
end end
end end

View file

@ -0,0 +1,192 @@
# 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.SimplePolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
setup do
orig = Config.get!(:mrf_simple)
Config.put(:mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
reject: [],
accept: []
)
on_exit(fn ->
Config.put(:mrf_simple, orig)
end)
end
describe "when :media_removal" do
test "is empty" do
Config.put([:mrf_simple, :media_removal], [])
media_message = build_media_message()
local_message = build_local_message()
assert SimplePolicy.filter(media_message) == {:ok, media_message}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
test "has a matching host" do
Config.put([:mrf_simple, :media_removal], ["remote.instance"])
media_message = build_media_message()
local_message = build_local_message()
assert SimplePolicy.filter(media_message) ==
{:ok,
media_message
|> Map.put("object", Map.delete(media_message["object"], "attachment"))}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
end
describe "when :media_nsfw" do
test "is empty" do
Config.put([:mrf_simple, :media_nsfw], [])
media_message = build_media_message()
local_message = build_local_message()
assert SimplePolicy.filter(media_message) == {:ok, media_message}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
test "has a matching host" do
Config.put([:mrf_simple, :media_nsfw], ["remote.instance"])
media_message = build_media_message()
local_message = build_local_message()
assert SimplePolicy.filter(media_message) ==
{:ok,
media_message
|> put_in(["object", "tag"], ["foo", "nsfw"])
|> put_in(["object", "sensitive"], true)}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
end
defp build_media_message do
%{
"actor" => "https://remote.instance/users/bob",
"type" => "Create",
"object" => %{
"attachment" => [%{}],
"tag" => ["foo"],
"sensitive" => false
}
}
end
describe "when :federated_timeline_removal" do
test "is empty" do
Config.put([:mrf_simple, :federated_timeline_removal], [])
{_, ftl_message} = build_ftl_actor_and_message()
local_message = build_local_message()
assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
test "has a matching host" do
{actor, ftl_message} = build_ftl_actor_and_message()
ftl_message_actor_host =
ftl_message
|> Map.fetch!("actor")
|> URI.parse()
|> Map.fetch!(:host)
Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host])
local_message = build_local_message()
assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
assert actor.follower_address in ftl_message["to"]
refute actor.follower_address in ftl_message["cc"]
refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]
assert SimplePolicy.filter(local_message) == {:ok, local_message}
end
end
defp build_ftl_actor_and_message do
actor = insert(:user)
{actor,
%{
"actor" => actor.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"],
"cc" => [actor.follower_address, "http://foo.bar/qux"]
}}
end
describe "when :reject" do
test "is empty" do
Config.put([:mrf_simple, :reject], [])
remote_message = build_remote_message()
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
test "has a matching host" do
Config.put([:mrf_simple, :reject], ["remote.instance"])
remote_message = build_remote_message()
assert SimplePolicy.filter(remote_message) == {:reject, nil}
end
end
describe "when :accept" do
test "is empty" do
Config.put([:mrf_simple, :accept], [])
local_message = build_local_message()
remote_message = build_remote_message()
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
test "is not empty but it doesn't have a matching host" do
Config.put([:mrf_simple, :accept], ["non.matching.remote"])
local_message = build_local_message()
remote_message = build_remote_message()
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:reject, nil}
end
test "has a matching host" do
Config.put([:mrf_simple, :accept], ["remote.instance"])
local_message = build_local_message()
remote_message = build_remote_message()
assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
end
end
defp build_local_message do
%{
"actor" => "#{Pleroma.Web.base_url()}/users/alice",
"to" => [],
"cc" => []
}
end
defp build_remote_message do
%{"actor" => "https://remote.instance/users/bob"}
end
end

View file

@ -96,6 +96,16 @@ test "visible_for_user?", %{
refute Visibility.visible_for_user?(direct, unrelated) refute Visibility.visible_for_user?(direct, unrelated)
end end
test "doesn't die when the user doesn't exist",
%{
direct: direct,
user: user
} do
Repo.delete(user)
Cachex.clear(:user_cache)
refute Visibility.is_private?(direct)
end
test "get_visibility", %{ test "get_visibility", %{
public: public, public: public,
private: private, private: private,

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
describe "/api/pleroma/admin/users" do describe "/api/pleroma/admin/users" do
@ -949,4 +951,329 @@ test "with token" do
} }
end end
end end
describe "GET /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
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 "PUT /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
[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]
})
%{conn: assign(conn, :user, admin), id: report_id}
end
test "mark report as resolved", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|> json_response(:ok)
assert response["state"] == "resolved"
end
test "closes report", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|> json_response(:ok)
assert response["state"] == "closed"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"})
assert json_response(conn, :bad_request) == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
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"])
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
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
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
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)
assert Enum.empty?(response["reports"])
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
end
test "returns 403 when requested by anonymous" do
conn =
build_conn()
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
test "returns created dm", %{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
|> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
"status" => "I will check it out"
})
|> json_response(:ok)
recipients = Enum.map(response["mentions"], & &1["username"])
assert conn.assigns[:user].nickname in recipients
assert reporter.nickname in recipients
assert response["content"] == "I will check it out"
assert response["visibility"] == "direct"
end
test "returns 400 when status is missing", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/reports/test/respond")
assert json_response(conn, :bad_request) == "Invalid parameters"
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/reports/test/respond", %{
"status" => "foo"
})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id}
end
test "toggle sensitive flag", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response(:ok)
assert response["sensitive"]
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} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|> json_response(:ok)
assert response["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 =
conn
|> put("/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 %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id}
end
test "deletes status", %{conn: conn, id: id} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
end
test "returns error when status is not exist", %{conn: conn} do
conn =
conn
|> delete("/api/pleroma/admin/statuses/test")
assert json_response(conn, :bad_request) == "Could not delete"
end
end
end end

View file

@ -272,10 +272,41 @@ test "creates a report" do
data: %{ data: %{
"type" => "Flag", "type" => "Flag",
"content" => ^comment, "content" => ^comment,
"object" => [^target_ap_id, ^activity_ap_id] "object" => [^target_ap_id, ^activity_ap_id],
"state" => "open"
} }
} = flag_activity } = flag_activity
end end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
end end
describe "reblog muting" do describe "reblog muting" do
@ -290,14 +321,14 @@ test "creates a report" do
test "add a reblog mute", %{muter: muter, muted: muted} do test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted) {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
assert Pleroma.User.showing_reblogs?(muter, muted) == false assert User.showing_reblogs?(muter, muted) == false
end end
test "remove a reblog mute", %{muter: muter, muted: muted} do test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted) {:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
{:ok, muter} = CommonAPI.show_reblogs(muter, muted) {:ok, muter} = CommonAPI.show_reblogs(muter, muted)
assert Pleroma.User.showing_reblogs?(muter, muted) == true assert User.showing_reblogs?(muter, muted) == true
end end
end end
end end

View file

@ -446,7 +446,7 @@ test "verify_credentials", %{conn: conn} do
end end
test "verify_credentials default scope unlisted", %{conn: conn} do test "verify_credentials default scope unlisted", %{conn: conn} do
user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "unlisted"}}) user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}})
conn = conn =
conn conn
@ -1322,7 +1322,7 @@ test "returns the relationships for the current user", %{conn: conn} do
describe "locked accounts" do describe "locked accounts" do
test "/api/v1/follow_requests works" do test "/api/v1/follow_requests works" do
user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user) other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user) {:ok, _activity} = ActivityPub.follow(other_user, user)
@ -1367,7 +1367,7 @@ test "/api/v1/follow_requests/:id/authorize works" do
end end
test "verify_credentials", %{conn: conn} do test "verify_credentials", %{conn: conn} do
user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "private"}}) user = insert(:user, %{info: %User.Info{default_scope: "private"}})
conn = conn =
conn conn
@ -1379,7 +1379,7 @@ test "verify_credentials", %{conn: conn} do
end end
test "/api/v1/follow_requests/:id/reject works" do test "/api/v1/follow_requests/:id/reject works" do
user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user) other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user) {:ok, _activity} = ActivityPub.follow(other_user, user)
@ -2129,7 +2129,7 @@ test "returns favorited DM only when user is logged in and he is one of recipien
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok) |> json_response(:ok)
assert length(anonymous_response) == 0 assert Enum.empty?(anonymous_response)
end end
test "does not return others' favorited DM when user is not one of recipients", %{ test "does not return others' favorited DM when user is not one of recipients", %{
@ -2153,7 +2153,7 @@ test "does not return others' favorited DM when user is not one of recipients",
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok) |> json_response(:ok)
assert length(response) == 0 assert Enum.empty?(response)
end end
test "paginates favorites using since_id and max_id", %{ test "paginates favorites using since_id and max_id", %{

View file

@ -355,7 +355,7 @@ test "tries to use the information in poco fields" do
{:ok, user} = OStatus.find_or_make_user(uri) {:ok, user} = OStatus.find_or_make_user(uri)
user = Pleroma.User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
assert user.name == "Constance Variable" assert user.name == "Constance Variable"
assert user.nickname == "lambadalambda@social.heldscal.la" assert user.nickname == "lambadalambda@social.heldscal.la"
assert user.local == false assert user.local == false
@ -374,7 +374,7 @@ test "find_or_make_user sets all the nessary input fields" do
{:ok, user} = OStatus.find_or_make_user(uri) {:ok, user} = OStatus.find_or_make_user(uri)
assert user.info == assert user.info ==
%Pleroma.User.Info{ %User.Info{
id: user.info.id, id: user.info.id,
ap_enabled: false, ap_enabled: false,
background: %{}, background: %{},
@ -407,7 +407,7 @@ test "find_make_or_update_user takes an author element and returns an updated us
{:ok, user} = OStatus.find_or_make_user(uri) {:ok, user} = OStatus.find_or_make_user(uri)
old_name = user.name old_name = user.name
old_bio = user.bio old_bio = user.bio
change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, old_name: nil}) change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, name: nil})
{:ok, user} = Repo.update(change) {:ok, user} = Repo.update(change)
refute user.avatar refute user.avatar