diff --git a/CHANGELOG.md b/CHANGELOG.md index 839bf90ab..9361fa260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Changed +- MFR policy to set global expiration for all local Create activities
API Changes - **Breaking:** Emoji API: changed methods and renamed routes. @@ -15,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** removed `with_move` parameter from notifications timeline. ### Added +- Chats: Added support for federated chats. For details, see the docs. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - Instance: Add `background_image` to configuration and `/api/v1/instance` - Instance: Extend `/api/v1/instance` with Pleroma-specific information. @@ -25,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit). - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required. - Mix task to create trusted OAuth App. +- Mix task to reset MFA for user accounts - Notifications: Added `follow_request` notification type. - Added `:reject_deletes` group to SimplePolicy - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances @@ -36,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add support for filtering replies in public and home timelines - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. +- OTP: Add command to reload emoji packs
### Fixed diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex index 22a06e472..15fd06c3d 100644 --- a/benchmarks/load_testing/fetcher.ex +++ b/benchmarks/load_testing/fetcher.ex @@ -52,12 +52,12 @@ defp render_views(user) do defp opts_for_home_timeline(user) do %{ - "blocking_user" => user, - "count" => "20", - "muting_user" => user, - "type" => ["Create", "Announce"], - "user" => user, - "with_muted" => "true" + blocking_user: user, + count: "20", + muting_user: user, + type: ["Create", "Announce"], + user: user, + with_muted: true } end @@ -70,17 +70,17 @@ defp fetch_home_timeline(user) do ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last() second_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", first_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id)) |> Enum.reverse() |> List.last() third_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", second_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id)) |> Enum.reverse() |> List.last() forth_page_last = - ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", third_page_last.id)) + ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id)) |> Enum.reverse() |> List.last() @@ -90,19 +90,19 @@ defp fetch_home_timeline(user) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id), - "1 page only media" => Map.put(opts, "only_media", "true"), + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id), + "1 page only media" => Map.put(opts, :only_media, true), "2 page only media" => - Map.put(opts, "max_id", first_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true), "3 page only media" => - Map.put(opts, "max_id", second_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true), "4 page only media" => - Map.put(opts, "max_id", third_page_last.id) |> Map.put("only_media", "true"), + Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true), "5 page only media" => - Map.put(opts, "max_id", forth_page_last.id) |> Map.put("only_media", "true") + Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true) }, formatters: formatters() ) @@ -110,12 +110,12 @@ defp fetch_home_timeline(user) do defp opts_for_direct_timeline(user) do %{ - :visibility => "direct", - "blocking_user" => user, - "count" => "20", - "type" => "Create", - "user" => user, - "with_muted" => "true" + visibility: "direct", + blocking_user: user, + count: "20", + type: "Create", + user: user, + with_muted: true } end @@ -130,7 +130,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts) |> List.last() - opts2 = Map.put(opts, "max_id", first_page_last.id) + opts2 = Map.put(opts, :max_id, first_page_last.id) second_page_last = recipients @@ -138,7 +138,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts2) |> List.last() - opts3 = Map.put(opts, "max_id", second_page_last.id) + opts3 = Map.put(opts, :max_id, second_page_last.id) third_page_last = recipients @@ -146,7 +146,7 @@ defp fetch_direct_timeline(user) do |> Pagination.fetch_paginated(opts3) |> List.last() - opts4 = Map.put(opts, "max_id", third_page_last.id) + opts4 = Map.put(opts, :max_id, third_page_last.id) forth_page_last = recipients @@ -165,7 +165,7 @@ defp fetch_direct_timeline(user) do "2 page" => opts2, "3 page" => opts3, "4 page" => opts4, - "5 page" => Map.put(opts4, "max_id", forth_page_last.id) + "5 page" => Map.put(opts4, :max_id, forth_page_last.id) }, formatters: formatters() ) @@ -173,34 +173,34 @@ defp fetch_direct_timeline(user) do defp opts_for_public_timeline(user) do %{ - "type" => ["Create", "Announce"], - "local_only" => false, - "blocking_user" => user, - "muting_user" => user + type: ["Create", "Announce"], + local_only: false, + blocking_user: user, + muting_user: user } end defp opts_for_public_timeline(user, :local) do %{ - "type" => ["Create", "Announce"], - "local_only" => true, - "blocking_user" => user, - "muting_user" => user + type: ["Create", "Announce"], + local_only: true, + blocking_user: user, + muting_user: user } end defp opts_for_public_timeline(user, :tag) do %{ - "blocking_user" => user, - "count" => "20", - "local_only" => nil, - "muting_user" => user, - "tag" => ["tag"], - "tag_all" => [], - "tag_reject" => [], - "type" => "Create", - "user" => user, - "with_muted" => "true" + blocking_user: user, + count: "20", + local_only: nil, + muting_user: user, + tag: ["tag"], + tag_all: [], + tag_reject: [], + type: "Create", + user: user, + with_muted: true } end @@ -223,7 +223,7 @@ defp fetch_public_timeline(user, :tag) do end defp fetch_public_timeline(user, :only_media) do - opts = opts_for_public_timeline(user) |> Map.put("only_media", "true") + opts = opts_for_public_timeline(user) |> Map.put(:only_media, true) fetch_public_timeline(opts, "public timeline only media") end @@ -245,15 +245,13 @@ defp fetch_public_timeline(user, :with_blocks) do user = User.get_by_id(user.id) - opts = Map.put(opts, "blocking_user", user) + opts = Map.put(opts, :blocking_user, user) - Benchee.run( - %{ - "public timeline with user block" => fn -> - ActivityPub.fetch_public_activities(opts) - end - }, - ) + Benchee.run(%{ + "public timeline with user block" => fn -> + ActivityPub.fetch_public_activities(opts) + end + }) domains = Enum.reduce(remote_non_friends, [], fn non_friend, domains -> @@ -269,30 +267,28 @@ defp fetch_public_timeline(user, :with_blocks) do end) user = User.get_by_id(user.id) - opts = Map.put(opts, "blocking_user", user) + opts = Map.put(opts, :blocking_user, user) - Benchee.run( - %{ - "public timeline with domain block" => fn opts -> - ActivityPub.fetch_public_activities(opts) - end - } - ) + Benchee.run(%{ + "public timeline with domain block" => fn -> + ActivityPub.fetch_public_activities(opts) + end + }) end defp fetch_public_timeline(opts, title) when is_binary(title) do first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() second_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", first_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id)) |> List.last() third_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", second_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id)) |> List.last() forth_page_last = - ActivityPub.fetch_public_activities(Map.put(opts, "max_id", third_page_last.id)) + ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id)) |> List.last() Benchee.run( @@ -303,17 +299,17 @@ defp fetch_public_timeline(opts, title) when is_binary(title) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id) + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id) }, formatters: formatters() ) end defp opts_for_notifications do - %{"count" => "20", "with_muted" => "true"} + %{count: "20", with_muted: true} end defp fetch_notifications(user) do @@ -322,15 +318,15 @@ defp fetch_notifications(user) do first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last() second_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", first_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id)) |> List.last() third_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", second_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id)) |> List.last() forth_page_last = - MastodonAPI.get_notifications(user, Map.put(opts, "max_id", third_page_last.id)) + MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id)) |> List.last() Benchee.run( @@ -341,10 +337,10 @@ defp fetch_notifications(user) do }, inputs: %{ "1 page" => opts, - "2 page" => Map.put(opts, "max_id", first_page_last.id), - "3 page" => Map.put(opts, "max_id", second_page_last.id), - "4 page" => Map.put(opts, "max_id", third_page_last.id), - "5 page" => Map.put(opts, "max_id", forth_page_last.id) + "2 page" => Map.put(opts, :max_id, first_page_last.id), + "3 page" => Map.put(opts, :max_id, second_page_last.id), + "4 page" => Map.put(opts, :max_id, third_page_last.id), + "5 page" => Map.put(opts, :max_id, forth_page_last.id) }, formatters: formatters() ) @@ -354,13 +350,13 @@ defp fetch_favourites(user) do first_page_last = ActivityPub.fetch_favourites(user) |> List.last() second_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => first_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last() third_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => second_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last() forth_page_last = - ActivityPub.fetch_favourites(user, %{"max_id" => third_page_last.id}) |> List.last() + ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last() Benchee.run( %{ @@ -370,10 +366,10 @@ defp fetch_favourites(user) do }, inputs: %{ "1 page" => %{}, - "2 page" => %{"max_id" => first_page_last.id}, - "3 page" => %{"max_id" => second_page_last.id}, - "4 page" => %{"max_id" => third_page_last.id}, - "5 page" => %{"max_id" => forth_page_last.id} + "2 page" => %{:max_id => first_page_last.id}, + "3 page" => %{:max_id => second_page_last.id}, + "4 page" => %{:max_id => third_page_last.id}, + "5 page" => %{:max_id => forth_page_last.id} }, formatters: formatters() ) @@ -381,8 +377,8 @@ defp fetch_favourites(user) do defp opts_for_long_thread(user) do %{ - "blocking_user" => user, - "user" => user + blocking_user: user, + user: user } end @@ -392,9 +388,9 @@ defp fetch_long_thread(user) do opts = opts_for_long_thread(user) - private_input = {private.data["context"], Map.put(opts, "exclude_id", private.id)} + private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)} - public_input = {public.data["context"], Map.put(opts, "exclude_id", public.id)} + public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)} Benchee.run( %{ @@ -514,13 +510,13 @@ defp render_long_thread(user) do public_context = ActivityPub.fetch_activities_for_context( public.data["context"], - Map.put(fetch_opts, "exclude_id", public.id) + Map.put(fetch_opts, :exclude_id, public.id) ) private_context = ActivityPub.fetch_activities_for_context( private.data["context"], - Map.put(fetch_opts, "exclude_id", private.id) + Map.put(fetch_opts, :exclude_id, private.id) ) Benchee.run( @@ -551,14 +547,14 @@ defp fetch_timelines_with_reply_filtering(user) do end, "Public timeline with reply filtering - following" => fn -> public_params - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() end, "Public timeline with reply filtering - self" => fn -> public_params - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() end }, @@ -577,16 +573,16 @@ defp fetch_timelines_with_reply_filtering(user) do "Home timeline with reply filtering - following" => fn -> private_params = private_params - |> Map.put("reply_filtering_user", user) - |> Map.put("reply_visibility", "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "following") ActivityPub.fetch_activities(recipients, private_params) end, "Home timeline with reply filtering - self" => fn -> private_params = private_params - |> Map.put("reply_filtering_user", user) - |> Map.put("reply_visibility", "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:reply_visibility, "self") ActivityPub.fetch_activities(recipients, private_params) end diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index 1162b2e06..c051335a5 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -100,14 +100,14 @@ defp hashtag_fetching(params, user, local_only) do _activities = params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) + |> Map.put(:type, "Create") + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:tag, tags) + |> Map.put(:tag_all, tag_all) + |> Map.put(:tag_reject, tag_reject) |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() end end diff --git a/config/config.exs b/config/config.exs index 6e75b79ec..32a92917c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -371,6 +371,8 @@ config :pleroma, :mrf_subchain, match_actor: %{} +config :pleroma, :mrf_activity_expiration, days: 365 + config :pleroma, :mrf_vocabulary, accept: [], reject: [] diff --git a/config/description.exs b/config/description.exs index 807c945e0..add1601e2 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1471,6 +1471,21 @@ } ] }, + %{ + group: :pleroma, + key: :mrf_activity_expiration, + label: "MRF Activity Expiration Policy", + type: :group, + description: "Adds expiration to all local Create Note activities", + children: [ + %{ + key: :days, + type: :integer, + description: "Default global expiration time for all local Create activities (in days)", + suggestions: [90, 365] + } + ] + }, %{ group: :pleroma, key: :mrf_subchain, diff --git a/docs/API/chats.md b/docs/API/chats.md new file mode 100644 index 000000000..aa6119670 --- /dev/null +++ b/docs/API/chats.md @@ -0,0 +1,248 @@ +# Chats + +Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common. + +## Why Chats? + +There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API. + +This is an awkward setup for a few reasons: + +- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much") +- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation. +- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message. +- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public. + +As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly. + +## Chats explained +For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview: + +- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future. +- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them. +- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field. +- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued. +- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person. +- `ChatMessage`s don't show up in the existing timelines. +- Chats can never go from private to public. They are always private between the two actors. + +## Caveats + +- Chats are NOT E2E encrypted (yet). Security is still the same as email. + +## API + +In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`. + +This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`. + +### Creating or getting a chat. + +To create or get an existing Chat for a certain recipient (identified by Account ID) +you can call: + +`POST /api/v1/pleroma/chats/by-account-id/:account_id` + +The account id is the normal FlakeId of the user +``` +POST /api/v1/pleroma/chats/by-account-id/someflakeid +``` + +If you already have the id of a chat, you can also use + +``` +GET /api/v1/pleroma/chats/:id +``` + +There will only ever be ONE Chat for you and a given recipient, so this call +will return the same Chat if you already have one with that user. + +Returned data: + +```json +{ + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2, + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" +} +``` + +### Marking a chat as read + +To mark a number of messages in a chat up to a certain message as read, you can use + +`POST /api/v1/pleroma/chats/:id/read` + + +Parameters: +- last_read_id: Given this id, all chat messages until this one will be marked as read. Required. + + +Returned data: + +```json +{ + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 0, + "updated_at": "2020-04-21T15:11:46.000Z" +} +``` + +### Marking a single chat message as read + +To set the `unread` property of a message to `false` + +`POST /api/v1/pleroma/chats/:id/messages/:message_id/read` + +Returned data: + +The modified chat message + +### Getting a list of Chats + +`GET /api/v1/pleroma/chats` + +This will return a list of chats that you have been involved in, sorted by their +last update (so new chats will be at the top). + +Returned data: + +```json +[ + { + "account": { + "id": "someflakeid", + "username": "somenick", + ... + }, + "id" : "1", + "unread" : 2, + "last_message" : {...}, // The last message in that chat + "updated_at": "2020-04-21T15:11:46.000Z" + } +] +``` + +The recipient of messages that are sent to this chat is given by their AP ID. +No pagination is implemented for now. + +### Getting the messages for a Chat + +For a given Chat id, you can get the associated messages with + +`GET /api/v1/pleroma/chats/:id/messages` + +This will return all messages, sorted by most recent to least recent. The usual +pagination options are implemented. + +Returned data: + +```json +[ + { + "account_id": "someflakeid", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13", + "unread": true + }, + { + "account_id": "someflakeid", + "chat_id": "1", + "content": "Whats' up?", + "created_at": "2020-04-21T15:06:45.000Z", + "emojis": [], + "id": "12", + "unread": false + } +] +``` + +### Posting a chat message + +Posting a chat message for given Chat id works like this: + +`POST /api/v1/pleroma/chats/:id/messages` + +Parameters: +- content: The text content of the message. Optional if media is attached. +- media_id: The id of an upload that will be attached to the message. + +Currently, no formatting beyond basic escaping and emoji is implemented. + +Returned data: + +```json +{ + "account_id": "someflakeid", + "chat_id": "1", + "content": "Check this out :firefox:", + "created_at": "2020-04-21T15:11:46.000Z", + "emojis": [ + { + "shortcode": "firefox", + "static_url": "https://dontbulling.me/emoji/Firefox.gif", + "url": "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker": false + } + ], + "id": "13", + "unread": false +} +``` + +### Deleting a chat message + +Deleting a chat message for given Chat id works like this: + +`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id` + +Returned data is the deleted message. + +### Notifications + +There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`: + +```json +{ + "id": "someid", + "type": "pleroma:chat_mention", + "account": { ... } // User account of the sender, + "chat_message": { + "chat_id": "1", + "id": "10", + "content": "Hello", + "account_id": "someflakeid", + "unread": false + }, + "created_at": "somedate" +} +``` + +### Streaming + +There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. + +### Web Push + +If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription. diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 434ade9a4..be3c802af 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API): Has these additional fields under the `pleroma` object: - `unread_count`: contains number unread notifications + +## Streaming + +There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field. diff --git a/docs/administration/CLI_tasks/emoji.md b/docs/administration/CLI_tasks/emoji.md index 3d524a52b..ddcb7e62c 100644 --- a/docs/administration/CLI_tasks/emoji.md +++ b/docs/administration/CLI_tasks/emoji.md @@ -44,3 +44,11 @@ Currently, only .zip archives are recognized as remote pack files and packs are The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted). + +## Reload emoji packs + +```sh tab="OTP" +./bin/pleroma_ctl emoji reload +``` + +This command only works with OTP releases. diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md index afeb8d52f..1e6f4a8b4 100644 --- a/docs/administration/CLI_tasks/user.md +++ b/docs/administration/CLI_tasks/user.md @@ -135,6 +135,16 @@ mix pleroma.user reset_password ``` +## Disable Multi Factor Authentication (MFA/2FA) for a user +```sh tab="OTP" + ./bin/pleroma_ctl user reset_mfa +``` + +```sh tab="From Source" +mix pleroma.user reset_mfa +``` + + ## Set the value of the given user's settings ```sh tab="OTP" ./bin/pleroma_ctl user set [option ...] diff --git a/docs/ap_extensions.md b/docs/ap_extensions.md new file mode 100644 index 000000000..c4550a1ac --- /dev/null +++ b/docs/ap_extensions.md @@ -0,0 +1,35 @@ +# ChatMessages + +ChatMessages are the messages sent in 1-on-1 chats. They are similar to +`Note`s, but the addresing is done by having a single AP actor in the `to` +field. Addressing multiple actors is not allowed. These messages are always +private, there is no public version of them. They are created with a `Create` +activity. + +Example: + +```json +{ + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad.", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" +} +``` + +This setup does not prevent multi-user chats, but these will have to go through +a `Group`, which will be the recipient of the messages and then `Announce` them +to the users in the `Group`. diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 505acb293..456762151 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -39,7 +39,7 @@ To add configuration to your config file, you can copy it from the base config. * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default). * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production. - * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)). + * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)). * `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive). * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)). * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)). @@ -49,7 +49,8 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)). * `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)). * `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)). -* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. + * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)). +* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``. * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML). @@ -154,6 +155,10 @@ config :pleroma, :mrf_user_allowlist, * `rejected_shortcodes`: Regex-list of shortcodes to reject * `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk +#### :mrf_activity_expiration + +* `days`: Default global expiration time for all local Create activities (in days) + ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index 688be3e71..d301ca615 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -37,18 +37,17 @@ server { listen 443 ssl http2; listen [::]:443 ssl http2; - ssl_session_timeout 5m; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; - # Add TLSv1.0 to support older devices - ssl_protocols TLSv1.2; - # Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.) - # ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; + ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_prefer_server_ciphers on; + ssl_prefer_server_ciphers off; # In case of an old server with an OpenSSL version of 1.0.2 or below, # leave only prime256v1 or comment out the following line. ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 29a5fa99c..f4eaeac98 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -237,6 +237,12 @@ def run(["gen-pack" | args]) do end end + def run(["reload"]) do + start_pleroma() + Pleroma.Emoji.reload() + IO.puts("Emoji packs have been reloaded.") + end + defp fetch_and_decode(from) do with {:ok, json} <- fetch(from) do Jason.decode!(json) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 3635c02bc..bca7e87bf 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -144,6 +144,18 @@ def run(["reset_password", nickname]) do end end + def run(["reset_mfa", nickname]) do + start_pleroma() + + with %User{local: true} = user <- User.get_cached_by_nickname(nickname), + {:ok, _token} <- Pleroma.MFA.disable(user) do + shell_info("Multi-Factor Authentication disabled for #{user.nickname}") + else + _ -> + shell_error("No local user #{nickname}") + end + end + def run(["deactivate", nickname]) do start_pleroma() diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 6213d0eb7..da1be20b3 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -24,16 +24,6 @@ defmodule Pleroma.Activity do @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} - # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 - @mastodon_notification_types %{ - "Create" => "mention", - "Follow" => ["follow", "follow_request"], - "Announce" => "reblog", - "Like" => "favourite", - "Move" => "move", - "EmojiReact" => "pleroma:emoji_reaction" - } - schema "activities" do field(:data, :map) field(:local, :boolean, default: true) @@ -300,32 +290,6 @@ def follow_accepted?( def follow_accepted?(_), do: false - @spec mastodon_notification_type(Activity.t()) :: String.t() | nil - - for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do - def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}), - do: unquote(type) - end - - def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do - if follow_accepted?(activity) do - "follow" - else - "follow_request" - end - end - - def mastodon_notification_type(%Activity{}), do: nil - - @spec from_mastodon_notification_type(String.t()) :: String.t() | nil - @doc "Converts Mastodon notification type to AR activity type" - def from_mastodon_notification_type(type) do - with {k, _v} <- - Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do - k - end - end - def all_by_actor_and_id(actor, status_ids \\ []) def all_by_actor_and_id(_actor, []), do: [] diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index 12d64c2fe..cd523cf7d 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -92,10 +92,10 @@ def handle_command(state, "home") do params = %{} - |> Map.put("type", ["Create"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = [user.ap_id | Pleroma.User.following(user)] diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex new file mode 100644 index 000000000..24a86371e --- /dev/null +++ b/lib/pleroma/chat.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat do + use Ecto.Schema + + import Ecto.Changeset + + alias Pleroma.Repo + alias Pleroma.User + + @moduledoc """ + Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet). + + It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages. + """ + + @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + + schema "chats" do + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + field(:recipient, :string) + + timestamps() + end + + def changeset(struct, params) do + struct + |> cast(params, [:user_id, :recipient]) + |> validate_change(:recipient, fn + :recipient, recipient -> + case User.get_cached_by_ap_id(recipient) do + nil -> [recipient: "must be an existing user"] + _ -> [] + end + end) + |> validate_required([:user_id, :recipient]) + |> unique_constraint(:user_id, name: :chats_user_id_recipient_index) + end + + def get_by_id(id) do + __MODULE__ + |> Repo.get(id) + end + + def get(user_id, recipient) do + __MODULE__ + |> Repo.get_by(user_id: user_id, recipient: recipient) + end + + def get_or_create(user_id, recipient) do + %__MODULE__{} + |> changeset(%{user_id: user_id, recipient: recipient}) + |> Repo.insert( + # Need to set something, otherwise we get nothing back at all + on_conflict: [set: [recipient: recipient]], + returning: true, + conflict_target: [:user_id, :recipient] + ) + end + + def bump_or_create(user_id, recipient) do + %__MODULE__{} + |> changeset(%{user_id: user_id, recipient: recipient}) + |> Repo.insert( + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + returning: true, + conflict_target: [:user_id, :recipient] + ) + end +end diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex new file mode 100644 index 000000000..131ae0186 --- /dev/null +++ b/lib/pleroma/chat/message_reference.ex @@ -0,0 +1,117 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat.MessageReference do + @moduledoc """ + A reference that builds a relation between an AP chat message that a user can see and whether it has been seen + by them, or should be displayed to them. Used to build the chat view that is presented to the user. + """ + + use Ecto.Schema + + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Repo + + import Ecto.Changeset + import Ecto.Query + + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} + + schema "chat_message_references" do + belongs_to(:object, Object) + belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType) + + field(:unread, :boolean, default: true) + + timestamps() + end + + def changeset(struct, params) do + struct + |> cast(params, [:object_id, :chat_id, :unread]) + |> validate_required([:object_id, :chat_id, :unread]) + end + + def get_by_id(id) do + __MODULE__ + |> Repo.get(id) + |> Repo.preload(:object) + end + + def delete(cm_ref) do + cm_ref + |> Repo.delete() + end + + def delete_for_object(%{id: object_id}) do + from(cr in __MODULE__, + where: cr.object_id == ^object_id + ) + |> Repo.delete_all() + end + + def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do + __MODULE__ + |> Repo.get_by(chat_id: chat_id, object_id: object_id) + |> Repo.preload(:object) + end + + def for_chat_query(chat) do + from(cr in __MODULE__, + where: cr.chat_id == ^chat.id, + order_by: [desc: :id], + preload: [:object] + ) + end + + def last_message_for_chat(chat) do + chat + |> for_chat_query() + |> limit(1) + |> Repo.one() + end + + def create(chat, object, unread) do + params = %{ + chat_id: chat.id, + object_id: object.id, + unread: unread + } + + %__MODULE__{} + |> changeset(params) + |> Repo.insert() + end + + def unread_count_for_chat(chat) do + chat + |> for_chat_query() + |> where([cmr], cmr.unread == true) + |> Repo.aggregate(:count) + end + + def mark_as_read(cm_ref) do + cm_ref + |> changeset(%{unread: false}) + |> Repo.update() + end + + def set_all_seen_for_chat(chat, last_read_id \\ nil) do + query = + chat + |> for_chat_query() + |> exclude(:order_by) + |> exclude(:preload) + |> where([cmr], cmr.unread == true) + + if last_read_id do + query + |> where([cmr], cmr.id <= ^last_read_id) + else + query + end + |> Repo.update_all(set: [unread: false]) + end +end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 51bb1bda9..ce7bd2396 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -163,8 +163,8 @@ def for_user_with_last_activity_id(user, params \\ %{}) do |> Enum.map(fn participation -> activity_id = ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) %{ diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3a3082e72..093b1f405 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -141,6 +141,12 @@ def following_query(%User{} = user) do |> where([r], r.state == ^:follow_accept) end + def outgoing_pending_follow_requests_query(%User{} = follower) do + __MODULE__ + |> where([r], r.follower_id == ^follower.id) + |> where([r], r.state == ^:follow_pending) + end + def following(%User{} = user) do following = following_query(user) diff --git a/lib/pleroma/helpers/uri_helper.ex b/lib/pleroma/helpers/uri_helper.ex index 69d8c8fe0..6d205a636 100644 --- a/lib/pleroma/helpers/uri_helper.ex +++ b/lib/pleroma/helpers/uri_helper.ex @@ -17,14 +17,6 @@ def append_uri_params(uri, appended_params) do |> URI.to_string() end - def append_param_if_present(%{} = params, param_name, param_value) do - if param_value do - Map.put(params, param_name, param_value) - else - params - end - end - def maybe_add_base("/" <> uri, base), do: Path.join([base, uri]) def maybe_add_base(uri, _base), do: uri end diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex new file mode 100644 index 000000000..ab2e32e2f --- /dev/null +++ b/lib/pleroma/maps.ex @@ -0,0 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Maps do + def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do + with false <- is_nil(key), + false <- is_nil(value), + {:ok, new_value} <- value_function.(value) do + Map.put(map, key, new_value) + else + _ -> map + end + end +end diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex new file mode 100644 index 000000000..09647d12a --- /dev/null +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelper.NotificationBackfill do + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Query + + def fill_in_notification_types do + query = + from(n in Pleroma.Notification, + where: is_nil(n.type), + preload: :activity + ) + + query + |> Repo.all() + |> Enum.each(fn notification -> + type = + notification.activity + |> type_from_activity() + + notification + |> Notification.changeset(%{type: type}) + |> Repo.update() + end) + end + + # This is copied over from Notifications to keep this stable. + defp type_from_activity(%{data: %{"type" => type}} = activity) do + case type do + "Follow" -> + accepted_function = fn activity -> + with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), + %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do + Pleroma.FollowingRelationship.following?(follower, followed) + end + end + + if accepted_function.(activity) do + "follow" + else + "follow_request" + end + + "Announce" -> + "reblog" + + "Like" -> + "favourite" + + "Move" -> + "move" + + "EmojiReact" -> + "pleroma:emoji_reaction" + + # Compatibility with old reactions + "EmojiReaction" -> + "pleroma:emoji_reaction" + + "Create" -> + activity + |> type_from_activity_object() + + t -> + raise "No notification type for activity type #{t}" + end + end + + defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention" + + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do + object = Object.get_by_ap_id(activity.data["object"]) + + case object && object.data["type"] do + "ChatMessage" -> "pleroma:chat_mention" + _ -> "mention" + end + end +end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 7eca55ac9..3386a1933 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -30,12 +30,29 @@ defmodule Pleroma.Notification do schema "notifications" do field(:seen, :boolean, default: false) + # This is an enum type in the database. If you add a new notification type, + # remember to add a migration to add it to the `notifications_type` enum + # as well. + field(:type, :string) belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) timestamps() end + def update_notification_type(user, activity) do + with %__MODULE__{} = notification <- + Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do + type = + activity + |> type_from_activity() + + notification + |> changeset(%{type: type}) + |> Repo.update() + end + end + @spec unread_notifications_count(User.t()) :: integer() def unread_notifications_count(%User{id: user_id}) do from(q in __MODULE__, @@ -44,9 +61,21 @@ def unread_notifications_count(%User{id: user_id}) do |> Repo.aggregate(:count, :id) end + @notification_types ~w{ + favourite + follow + follow_request + mention + move + pleroma:chat_mention + pleroma:emoji_reaction + reblog + } + def changeset(%Notification{} = notification, attrs) do notification - |> cast(attrs, [:seen]) + |> cast(attrs, [:seen, :type]) + |> validate_inclusion(:type, @notification_types) end @spec last_read_query(User.t()) :: Ecto.Queryable.t() @@ -300,42 +329,95 @@ def dismiss(%{id: user_id} = _user, id) do end end - def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do - object = Object.normalize(activity) + def create_notifications(activity, options \\ []) + + def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do + object = Object.normalize(activity, false) if object && object.data["type"] == "Answer" do {:ok, []} else - do_create_notifications(activity) + do_create_notifications(activity, options) end end - def create_notifications(%Activity{data: %{"type" => type}} = activity) + def create_notifications(%Activity{data: %{"type" => type}} = activity, options) when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do - do_create_notifications(activity) + do_create_notifications(activity, options) end - def create_notifications(_), do: {:ok, []} + def create_notifications(_, _), do: {:ok, []} + + defp do_create_notifications(%Activity{} = activity, options) do + do_send = Keyword.get(options, :do_send, true) - defp do_create_notifications(%Activity{} = activity) do {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity) potential_receivers = enabled_receivers ++ disabled_receivers notifications = Enum.map(potential_receivers, fn user -> - do_send = user in enabled_receivers + do_send = do_send && user in enabled_receivers create_notification(activity, user, do_send) end) {:ok, notifications} end + defp type_from_activity(%{data: %{"type" => type}} = activity) do + case type do + "Follow" -> + if Activity.follow_accepted?(activity) do + "follow" + else + "follow_request" + end + + "Announce" -> + "reblog" + + "Like" -> + "favourite" + + "Move" -> + "move" + + "EmojiReact" -> + "pleroma:emoji_reaction" + + # Compatibility with old reactions + "EmojiReaction" -> + "pleroma:emoji_reaction" + + "Create" -> + activity + |> type_from_activity_object() + + t -> + raise "No notification type for activity type #{t}" + end + end + + defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention" + + defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do + object = Object.get_by_ap_id(activity.data["object"]) + + case object && object.data["type"] do + "ChatMessage" -> "pleroma:chat_mention" + _ -> "mention" + end + end + # TODO move to sql, too. def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do unless skip?(activity, user) do {:ok, %{notification: notification}} = Multi.new() - |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity}) + |> Multi.insert(:notification, %Notification{ + user_id: user.id, + activity: activity, + type: type_from_activity(activity) + }) |> Marker.multi_set_last_read_id(user, "notifications") |> Repo.transaction() @@ -527,4 +609,12 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, end def skip?(_, _, _), do: false + + def for_user_and_activity(user, activity) do + from(n in __MODULE__, + where: n.user_id == ^user.id, + where: n.activity_id == ^activity.id + ) + |> Repo.one() + end end diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index d43a96cd2..1b99e44f9 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -23,12 +23,12 @@ def page_keys, do: @page_keys @spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) - def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do total = Repo.aggregate(query, :count, :id) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding) } end @@ -41,7 +41,7 @@ def fetch_paginated(query, params, :keyset, table_binding) do |> enforce_order(options) end - def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do + def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do total = query |> Ecto.Query.exclude(:left_join) @@ -49,7 +49,7 @@ def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) %{ total: total, - items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding) + items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding) } end @@ -90,12 +90,6 @@ defp cast_params(params) do skip_order: :boolean } - params = - Enum.reduce(params, %{}, fn - {key, _value}, acc when is_atom(key) -> Map.drop(acc, [key]) - {key, value}, acc -> Map.put(acc, key, value) - end) - changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset.changes end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index 6a339b32c..1420a9611 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -113,6 +113,10 @@ defp get_proxy_and_attachment_sources do add_source(acc, host) end) + media_proxy_base_url = + if Config.get([:media_proxy, :base_url]), + do: URI.parse(Config.get([:media_proxy, :base_url])).host + upload_base_url = if Config.get([Pleroma.Upload, :base_url]), do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host @@ -122,6 +126,7 @@ defp get_proxy_and_attachment_sources do do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host [] + |> add_source(media_proxy_base_url) |> add_source(upload_base_url) |> add_source(s3_endpoint) |> add_source(media_proxy_whitelist) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 1be1a3a5b..797555bff 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -67,6 +67,7 @@ def store(upload, opts \\ []) do {:ok, %{ "type" => opts.activity_type, + "mediaType" => upload.content_type, "url" => [ %{ "type" => "Link", diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 72ee2d58e..c5c74d132 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1489,6 +1489,8 @@ def perform(:delete, %User{} = user) do delete_user_activities(user) + delete_outgoing_pending_follow_requests(user) + delete_or_deactivate(user) end @@ -1611,6 +1613,12 @@ defp delete_activity(%{data: %{"type" => type}} = activity, user) defp delete_activity(_activity, _user), do: "Doing nothing" + defp delete_outgoing_pending_follow_requests(user) do + user + |> FollowingRelationship.outgoing_pending_follow_requests_query() + |> Repo.delete_all() + end + def html_filter_policy(%User{no_rich_text: true}) do Pleroma.HTML.Scrubber.TwitterText end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 958f3e5af..e7d0a9caa 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -5,10 +5,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics + alias Pleroma.ActivityExpiration alias Pleroma.Config alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment @@ -19,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Streamer alias Pleroma.Web.WebFinger alias Pleroma.Workers.BackgroundWorker @@ -31,25 +32,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do require Logger require Pleroma.Constants - # For Announce activities, we filter the recipients based on following status for any actors - # that match actual users. See issue #164 for more information about why this is necessary. - defp get_recipients(%{"type" => "Announce"} = data) do - to = Map.get(data, "to", []) - cc = Map.get(data, "cc", []) - bcc = Map.get(data, "bcc", []) - actor = User.get_cached_by_ap_id(data["actor"]) - - recipients = - Enum.filter(Enum.concat([to, cc, bcc]), fn recipient -> - case User.get_cached_by_ap_id(recipient) do - nil -> true - user -> User.following?(user, actor) - end - end) - - {recipients, to, cc} - end - defp get_recipients(%{"type" => "Create"} = data) do to = Map.get(data, "to", []) cc = Map.get(data, "cc", []) @@ -67,16 +49,12 @@ defp get_recipients(data) do {recipients, to, cc} end - defp check_actor_is_active(actor) do - if not is_nil(actor) do - with user <- User.get_cached_by_ap_id(actor), - false <- user.deactivated do - true - else - _e -> false - end - else - true + defp check_actor_is_active(nil), do: true + + defp check_actor_is_active(actor) when is_binary(actor) do + case User.get_cached_by_ap_id(actor) do + %User{deactivated: deactivated} -> not deactivated + _ -> false end end @@ -87,7 +65,7 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil( defp check_remote_limit(_), do: true - def increase_note_count_if_public(actor, object) do + defp increase_note_count_if_public(actor, object) do if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor} end @@ -95,38 +73,35 @@ def decrease_note_count_if_public(actor, object) do if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor} end - def increase_replies_count_if_reply(%{ - "object" => %{"inReplyTo" => reply_ap_id} = object, - "type" => "Create" - }) do + defp increase_replies_count_if_reply(%{ + "object" => %{"inReplyTo" => reply_ap_id} = object, + "type" => "Create" + }) do if is_public?(object) do Object.increase_replies_count(reply_ap_id) end end - def increase_replies_count_if_reply(_create_data), do: :noop + defp increase_replies_count_if_reply(_create_data), do: :noop - def decrease_replies_count_if_reply(%Object{ - data: %{"inReplyTo" => reply_ap_id} = object - }) do - if is_public?(object) do - Object.decrease_replies_count(reply_ap_id) - end - end - - def decrease_replies_count_if_reply(_object), do: :noop - - def increase_poll_votes_if_vote(%{ - "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, - "type" => "Create", - "actor" => actor - }) do + defp increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create", + "actor" => actor + }) do Object.increase_vote_count(reply_ap_id, name, actor) end - def increase_poll_votes_if_vote(_create_data), do: :noop + defp increase_poll_votes_if_vote(_create_data), do: :noop + @object_types ["ChatMessage"] @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} + def persist(%{"type" => type} = object, meta) when type in @object_types do + with {:ok, object} <- Object.create(object) do + {:ok, object, meta} + end + end + def persist(object, meta) do with local <- Keyword.fetch!(meta, :local), {recipients, _, _} <- get_recipients(object), @@ -153,20 +128,17 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when {:containment, :ok} <- {:containment, Containment.contain_child(map)}, {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = - Repo.insert(%Activity{ + %Activity{ data: map, local: local, actor: map["actor"], recipients: recipients - }) + } + |> Repo.insert() + |> maybe_create_activity_expiration() # Splice in the child object if we have one. - activity = - if not is_nil(object) do - Map.put(activity, :object, object) - else - activity - end + activity = Maps.put_if_present(activity, :object, object) BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id}) @@ -201,10 +173,18 @@ def notify_and_stream(activity) do stream_out_participations(participations) end + defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do + with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do + {:ok, activity} + end + end + + defp maybe_create_activity_expiration(result), do: result + defp create_or_bump_conversation(activity, actor) do with {:ok, conversation} <- Conversation.create_or_bump_for(activity), - %User{} = user <- User.get_cached_by_ap_id(actor), - Participation.mark_as_read(user, conversation) do + %User{} = user <- User.get_cached_by_ap_id(actor) do + Participation.mark_as_read(user, conversation) {:ok, conversation} end end @@ -226,13 +206,15 @@ def stream_out_participations(participations) do end def stream_out_participations(%Object{data: %{"context" => context}}, user) do - with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), - conversation = Repo.preload(conversation, :participations), - last_activity_id = - fetch_latest_activity_id_for_context(conversation.ap_id, %{ - "user" => user, - "blocking_user" => user - }) do + with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do + conversation = Repo.preload(conversation, :participations) + + last_activity_id = + fetch_latest_activity_id_for_context(conversation.ap_id, %{ + user: user, + blocking_user: user + }) + if last_activity_id do stream_out_participations(conversation.participations) end @@ -266,12 +248,13 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param published = params[:published] quick_insert? = Config.get([:env]) == :benchmark - with create_data <- - make_create_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(create_data, local, fake), + create_data = + make_create_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), _ <- increase_poll_votes_if_vote(create_data), @@ -299,12 +282,13 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d local = !(params[:local] == false) published = params[:published] - with listen_data <- - make_listen_data( - %{to: to, actor: actor, published: published, context: context, object: object}, - additional - ), - {:ok, activity} <- insert(listen_data, local), + listen_data = + make_listen_data( + %{to: to, actor: actor, published: published, context: context, object: object}, + additional + ) + + with {:ok, activity} <- insert(listen_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -322,14 +306,15 @@ def reject(params) do end @spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()} - def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do + defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do local = Map.get(params, :local, true) activity_id = Map.get(params, :activity_id, nil) - with data <- - %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} - |> Utils.maybe_put("id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} + |> Maps.put_if_present("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -341,34 +326,38 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do local = !(params[:local] == false) activity_id = params[:activity_id] - with data <- %{ - "to" => to, - "cc" => cc, - "type" => "Update", - "actor" => actor, - "object" => object - }, - data <- Utils.maybe_put(data, "id", activity_id), - {:ok, activity} <- insert(data, local), + data = + %{ + "to" => to, + "cc" => cc, + "type" => "Update", + "actor" => actor, + "object" => object + } + |> Maps.put_if_present("id", activity_id) + + with {:ok, activity} <- insert(data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} end end - @spec follow(User.t(), User.t(), String.t() | nil, boolean()) :: + @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: {:ok, Activity.t()} | {:error, any()} - def follow(follower, followed, activity_id \\ nil, local \\ true) do + def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do with {:ok, result} <- - Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do + Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do result end end - defp do_follow(follower, followed, activity_id, local) do - with data <- make_follow_data(follower, followed, activity_id), - {:ok, activity} <- insert(data, local), - _ <- notify_and_stream(activity), + defp do_follow(follower, followed, activity_id, local, opts) do + skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) + data = make_follow_data(follower, followed, activity_id) + + with {:ok, activity} <- insert(data, local), + _ <- skip_notify_and_stream || notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} else @@ -411,13 +400,13 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do defp do_block(blocker, blocked, activity_id, local) do unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - if unfollow_blocked do - follow_activity = fetch_latest_follow(blocker, blocked) - if follow_activity, do: unfollow(blocker, blocked, nil, local) + if unfollow_blocked and fetch_latest_follow(blocker, blocked) do + unfollow(blocker, blocked, nil, local) end - with block_data <- make_block_data(blocker, blocked, activity_id), - {:ok, activity} <- insert(block_data, local), + block_data = make_block_data(blocker, blocked, activity_id) + + with {:ok, activity} <- insert(block_data, local), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -496,8 +485,8 @@ def fetch_activities_for_context_query(context, opts) do public = [Constants.as_public()] recipients = - if opts["user"], - do: [opts["user"].ap_id | User.following(opts["user"])] ++ public, + if opts[:user], + do: [opts[:user].ap_id | User.following(opts[:user])] ++ public, else: public from(activity in Activity) @@ -505,7 +494,7 @@ def fetch_activities_for_context_query(context, opts) do |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> where( [activity], fragment( @@ -532,7 +521,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do FlakeId.Ecto.CompatType.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) + |> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() @@ -540,24 +529,18 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do @spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do - opts = Map.drop(opts, ["user"]) + opts = Map.delete(opts, :user) - query = fetch_activities_query([Constants.as_public()], opts) - - query = - if opts["restrict_unlisted"] do - restrict_unlisted(query) - else - query - end - - Pagination.fetch_paginated(query, opts, pagination) + [Constants.as_public()] + |> fetch_activities_query(opts) + |> restrict_unlisted(opts) + |> Pagination.fetch_paginated(opts, pagination) end @spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()] def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do opts - |> Map.put("restrict_unlisted", true) + |> Map.put(:restrict_unlisted, true) |> fetch_public_or_unlisted_activities(pagination) end @@ -566,20 +549,17 @@ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do defp restrict_visibility(query, %{visibility: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do - query = - from( - a in query, - where: - fragment( - "activity_visibility(?, ?, ?) = ANY (?)", - a.actor, - a.recipients, - a.data, - ^visibility - ) - ) - - query + from( + a in query, + where: + fragment( + "activity_visibility(?, ?, ?) = ANY (?)", + a.actor, + a.recipients, + a.data, + ^visibility + ) + ) else Logger.error("Could not restrict visibility to #{visibility}") end @@ -601,7 +581,7 @@ defp restrict_visibility(_query, %{visibility: visibility}) defp restrict_visibility(query, _visibility), do: query - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when is_list(visibility) do if Enum.all?(visibility, &(&1 in @valid_visibilities)) do from( @@ -621,7 +601,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) end end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility in @valid_visibilities do from( a in query, @@ -636,7 +616,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) ) end - defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + defp exclude_visibility(query, %{exclude_visibilities: visibility}) when visibility not in [nil | @valid_visibilities] do Logger.error("Could not exclude visibility to #{visibility}") query @@ -647,14 +627,10 @@ defp exclude_visibility(query, _visibility), do: query defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), do: query - defp restrict_thread_visibility( - query, - %{"user" => %User{skip_thread_containment: true}}, - _ - ), - do: query + defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _), + do: query - defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do + defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do from( a in query, where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) @@ -666,87 +642,99 @@ defp restrict_thread_visibility(query, _, _), do: query def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_user_activities(user, reading_user, params \\ %{}) do params = params - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("user", reading_user) - |> Map.put("actor_id", user.ap_id) - |> Map.put("pinned_activity_ids", user.pinned_activities) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:user, reading_user) + |> Map.put(:actor_id, user.ap_id) + |> Map.put(:pinned_activity_ids, user.pinned_activities) params = if User.blocks?(reading_user, user) do params else params - |> Map.put("blocking_user", reading_user) - |> Map.put("muting_user", reading_user) + |> Map.put(:blocking_user, reading_user) + |> Map.put(:muting_user, reading_user) end - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params) |> Enum.reverse() end def fetch_statuses(reading_user, params) do - params = - params - |> Map.put("type", ["Create", "Announce"]) + params = Map.put(params, :type, ["Create", "Announce"]) - recipients = - user_activities_recipients(%{ - "godmode" => params["godmode"], - "reading_user" => reading_user - }) - - fetch_activities(recipients, params, :offset) + %{ + godmode: params[:godmode], + reading_user: reading_user + } + |> user_activities_recipients() + |> fetch_activities(params, :offset) |> Enum.reverse() end - defp user_activities_recipients(%{"godmode" => true}) do - [] - end + defp user_activities_recipients(%{godmode: true}), do: [] - defp user_activities_recipients(%{"reading_user" => reading_user}) do + defp user_activities_recipients(%{reading_user: reading_user}) do if reading_user do - [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)] + [Constants.as_public(), reading_user.ap_id | User.following(reading_user)] else [Constants.as_public()] end end - defp restrict_since(query, %{"since_id" => ""}), do: query + defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do + raise "Can't use the child object without preloading!" + end - defp restrict_since(query, %{"since_id" => since_id}) do + defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do + from( + [activity, object] in query, + where: + fragment( + "?->>'type' != ? or ?->>'actor' != ?", + activity.data, + "Announce", + object.data, + ^actor + ) + ) + end + + defp restrict_announce_object_actor(query, _), do: query + + defp restrict_since(query, %{since_id: ""}), do: query + + defp restrict_since(query, %{since_id: since_id}) do from(activity in query, where: activity.id > ^since_id) end defp restrict_since(query, _), do: query - defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do + defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) - when is_list(tag_reject) and tag_reject != [] do + defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do from( [_activity, object] in query, where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject) @@ -755,12 +743,11 @@ defp restrict_tag_reject(query, %{"tag_reject" => tag_reject}) defp restrict_tag_reject(query, _), do: query - defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do + defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag_all(query, %{"tag_all" => tag_all}) - when is_list(tag_all) and tag_all != [] do + defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all) @@ -769,18 +756,18 @@ defp restrict_tag_all(query, %{"tag_all" => tag_all}) defp restrict_tag_all(query, _), do: query - defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do + defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do + defp restrict_tag(query, %{tag: tag}) when is_list(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag) ) end - defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do + defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do from( [_activity, object] in query, where: fragment("(?)->'tag' \\? (?)", object.data, ^tag) @@ -803,35 +790,35 @@ defp restrict_recipients(query, recipients, user) do ) end - defp restrict_local(query, %{"local_only" => true}) do + defp restrict_local(query, %{local_only: true}) do from(activity in query, where: activity.local == true) end defp restrict_local(query, _), do: query - defp restrict_actor(query, %{"actor_id" => actor_id}) do + defp restrict_actor(query, %{actor_id: actor_id}) do from(activity in query, where: activity.actor == ^actor_id) end defp restrict_actor(query, _), do: query - defp restrict_type(query, %{"type" => type}) when is_binary(type) do + defp restrict_type(query, %{type: type}) when is_binary(type) do from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type)) end - defp restrict_type(query, %{"type" => type}) do + defp restrict_type(query, %{type: type}) do from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type)) end defp restrict_type(query, _), do: query - defp restrict_state(query, %{"state" => state}) do + 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( [_activity, object] in query, where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id) @@ -840,11 +827,11 @@ defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, _), do: query - defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do + defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do raise "Can't use the child object without preloading!" end - defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do + defp restrict_media(query, %{only_media: true}) do from( [_activity, object] in query, where: fragment("not (?)->'attachment' = (?)", object.data, ^[]) @@ -853,7 +840,7 @@ defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1 defp restrict_media(query, _), do: query - defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do + defp restrict_replies(query, %{exclude_replies: true}) do from( [_activity, object] in query, where: fragment("?->>'inReplyTo' is null", object.data) @@ -861,8 +848,8 @@ defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "tr end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "self" + reply_filtering_user: user, + reply_visibility: "self" }) do from( [activity, object] in query, @@ -877,8 +864,8 @@ defp restrict_replies(query, %{ end defp restrict_replies(query, %{ - "reply_filtering_user" => user, - "reply_visibility" => "following" + reply_filtering_user: user, + reply_visibility: "following" }) do from( [activity, object] in query, @@ -897,16 +884,16 @@ defp restrict_replies(query, %{ defp restrict_replies(query, _), do: query - defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do + defp restrict_reblogs(query, %{exclude_reblogs: true}) do from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) end defp restrict_reblogs(query, _), do: query - defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query + defp restrict_muted(query, %{with_muted: true}), do: query - defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do - mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user) + defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do + mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user) query = from([activity] in query, @@ -914,7 +901,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes) ) - unless opts["skip_preload"] do + unless opts[:skip_preload] do from([thread_mute: tm] in query, where: is_nil(tm.user_id)) else query @@ -923,8 +910,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do - blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) + defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do + blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) domain_blocks = user.domain_blocks || [] following_ap_ids = User.get_friends_ap_ids(user) @@ -970,7 +957,7 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do defp restrict_blocked(query, _), do: query - defp restrict_unlisted(query) do + defp restrict_unlisted(query, %{restrict_unlisted: true}) do from( activity in query, where: @@ -982,19 +969,16 @@ defp restrict_unlisted(query) do ) end - # TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only, - # the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2' - # and `restrict_muted/2` + defp restrict_unlisted(query, _), do: query - defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids}) - when pinned in [true, "true", "1"] do + defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do from(activity in query, where: activity.id in ^ids) end defp restrict_pinned(query, _), do: query - defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do - muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user) + defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do + muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user) from( activity in query, @@ -1010,7 +994,7 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do defp restrict_muted_reblogs(query, _), do: query - defp restrict_instance(query, %{"instance" => instance}) do + defp restrict_instance(query, %{instance: instance}) do users = from( u in User, @@ -1024,7 +1008,7 @@ defp restrict_instance(query, %{"instance" => instance}) do defp restrict_instance(query, _), do: query - defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do if has_named_binding?(query, :object) do @@ -1036,7 +1020,19 @@ defp exclude_poll_votes(query, _) do end end - defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query + defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query + + defp exclude_chat_messages(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage") + ) + else + query + end + end + + defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query defp exclude_invisible_actors(query, _opts) do invisible_ap_ids = @@ -1047,38 +1043,38 @@ defp exclude_invisible_actors(query, _opts) do from([activity] in query, where: activity.actor not in ^invisible_ap_ids) end - defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do + defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do from(activity in query, where: activity.id != ^id) end defp exclude_id(query, _), do: query - defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query + defp maybe_preload_objects(query, %{skip_preload: true}), do: query defp maybe_preload_objects(query, _) do query |> Activity.with_preloaded_object() end - defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query defp maybe_preload_bookmarks(query, opts) do query - |> Activity.with_preloaded_bookmark(opts["user"]) + |> Activity.with_preloaded_bookmark(opts[:user]) end - defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do + defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do query |> Activity.with_preloaded_report_notes() end defp maybe_preload_report_notes(query, _), do: query - defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query + defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query defp maybe_set_thread_muted_field(query, opts) do query - |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"]) + |> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user]) end defp maybe_order(query, %{order: :desc}) do @@ -1094,24 +1090,23 @@ defp maybe_order(query, %{order: :asc}) do defp maybe_order(query, _), do: query defp fetch_activities_query_ap_ids_ops(opts) do - source_user = opts["muting_user"] + source_user = opts[:muting_user] ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: [] ap_id_relationships = - ap_id_relationships ++ - if opts["blocking_user"] && opts["blocking_user"] == source_user do - [:block] - else - [] - end + if opts[:blocking_user] && opts[:blocking_user] == source_user do + [:block | ap_id_relationships] + else + ap_id_relationships + end preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships) - restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) - restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) + restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) + restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts) restrict_muted_reblogs_opts = - Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts) {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} end @@ -1130,7 +1125,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> maybe_preload_report_notes(opts) |> maybe_set_thread_muted_field(opts) |> maybe_order(opts) - |> restrict_recipients(recipients, opts["user"]) + |> restrict_recipients(recipients, opts[:user]) |> restrict_replies(opts) |> restrict_tag(opts) |> restrict_tag_reject(opts) @@ -1150,19 +1145,21 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) + |> restrict_announce_object_actor(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_chat_messages(opts) |> exclude_invisible_actors(opts) |> exclude_visibility(opts) end def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do - list_memberships = Pleroma.List.memberships(opts["user"]) + list_memberships = Pleroma.List.memberships(opts[:user]) fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts, pagination) |> Enum.reverse() - |> maybe_update_cc(list_memberships, opts["user"]) + |> maybe_update_cc(list_memberships, opts[:user]) end @doc """ @@ -1178,16 +1175,15 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do |> select([_like, object, activity], %{activity | object: object}) |> order_by([like, _, _], desc_nulls_last: like.id) |> Pagination.fetch_paginated( - Map.merge(params, %{"skip_order" => true}), + Map.merge(params, %{skip_order: true}), pagination, :object_activity ) end - defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) - when is_list(list_memberships) and length(list_memberships) > 0 do + defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do Enum.map(activities, fn - %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + %{data: %{"bcc" => [_ | _] = bcc}} = activity -> if Enum.any?(bcc, &(&1 in list_memberships)) do update_in(activity.data["cc"], &[user_ap_id | &1]) else @@ -1201,7 +1197,7 @@ defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) defp maybe_update_cc(activities, _, _), do: activities - def fetch_activities_bounded_query(query, recipients, recipients_with_public) do + defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do from(activity in query, where: fragment("? && ?", activity.recipients, ^recipients) or @@ -1225,12 +1221,7 @@ def fetch_activities_bounded( @spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()} def upload(file, opts \\ []) do with {:ok, data} <- Upload.store(file, opts) do - obj_data = - if opts[:actor] do - Map.put(data, "actor", opts[:actor]) - else - data - end + obj_data = Maps.put_if_present(data, "actor", opts[:actor]) Repo.insert(%Object{data: obj_data}) end @@ -1276,8 +1267,8 @@ defp object_to_user_data(data) do %{"type" => "Emoji"} -> true _ -> false end) - |> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc -> - Map.put(acc, String.trim(name, ":"), url) + |> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} end) locked = data["manuallyApprovesFollowers"] || false @@ -1323,18 +1314,15 @@ defp object_to_user_data(data) do } # nickname can be nil because of virtual actors - user_data = - if data["preferredUsername"] do - Map.put( - user_data, - :nickname, - "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" - ) - else - Map.put(user_data, :nickname, nil) - end - - {:ok, user_data} + if data["preferredUsername"] do + Map.put( + user_data, + :nickname, + "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}" + ) + else + Map.put(user_data, :nickname, nil) + end end def fetch_follow_information_for_user(user) do @@ -1409,9 +1397,8 @@ defp collection_private(%{"first" => first}) do defp collection_private(_data), do: {:ok, true} def user_data_from_user_object(data) do - with {:ok, data} <- MRF.filter(data), - {:ok, data} <- object_to_user_data(data) do - {:ok, data} + with {:ok, data} <- MRF.filter(data) do + {:ok, object_to_user_data(data)} else e -> {:error, e} end @@ -1419,15 +1406,14 @@ def user_data_from_user_object(data) do def fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), - {:ok, data} <- user_data_from_user_object(data), - data <- maybe_update_follow_information(data) do - {:ok, data} + {:ok, data} <- user_data_from_user_object(data) do + {:ok, maybe_update_follow_information(data)} else - {:error, "Object has been deleted"} = e -> + {:error, "Object has been deleted" = e} -> Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} - e -> + {:error, e} -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") {:error, e} end @@ -1450,8 +1436,6 @@ def make_user_from_ap_id(ap_id) do |> Repo.insert() |> User.set_cache() end - else - e -> {:error, e} end end end @@ -1465,7 +1449,7 @@ def make_user_from_nickname(nickname) do end # filter out broken threads - def contain_broken_threads(%Activity{} = activity, %User{} = user) do + defp contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) end @@ -1476,7 +1460,7 @@ def contain_activity(%Activity{} = activity, %User{} = user) do def fetch_direct_messages_query do Activity - |> restrict_type(%{"type" => "Create"}) + |> restrict_type(%{type: "Create"}) |> restrict_visibility(%{visibility: "direct"}) |> order_by([activity], asc: activity.id) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 28727d619..f0b5c6e93 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.ControllerHelper alias Pleroma.Web.Endpoint alias Pleroma.Web.FederatingPlug alias Pleroma.Web.Federator @@ -230,27 +231,23 @@ def outbox( when page? in [true, "true"] do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do - activities = - if params["max_id"] do - ActivityPub.fetch_user_activities(user, for_user, %{ - "max_id" => params["max_id"], - # This is a hack because postgres generates inefficient queries when filtering by - # 'Answer', poll votes will be hidden by the visibility filter in this case anyway - "include_poll_votes" => true, - "limit" => 10 - }) - else - ActivityPub.fetch_user_activities(user, for_user, %{ - "limit" => 10, - "include_poll_votes" => true - }) - end + # "include_poll_votes" is a hack because postgres generates inefficient + # queries when filtering by 'Answer', poll votes will be hidden by the + # visibility filter in this case anyway + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("include_poll_votes", true) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + + activities = ActivityPub.fetch_user_activities(user, for_user, params) conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/outbox" }) end @@ -353,21 +350,24 @@ def read_inbox( %{"nickname" => nickname, "page" => page?} = params ) when page? in [true, "true"] do + params = + params + |> Map.drop(["nickname", "page"]) + |> Map.put("blocking_user", user) + |> Map.put("user", user) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + activities = - if params["max_id"] do - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ - "max_id" => params["max_id"], - "limit" => 10 - }) - else - ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10}) - end + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + |> Enum.reverse() conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) |> render("activity_collection_page.json", %{ activities: activities, + pagination: ControllerHelper.get_pagination_fields(conn, activities), iri: "#{user.ap_id}/inbox" }) end diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 51b74414a..1aac62c69 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do This module encodes our addressing policies and general shape of our objects. """ + alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.Relay @@ -65,6 +66,42 @@ def delete(actor, object_id) do }, []} end + def create(actor, object, recipients) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "actor" => actor.ap_id, + "to" => recipients, + "object" => object, + "type" => "Create", + "published" => DateTime.utc_now() |> DateTime.to_iso8601() + }, []} + end + + def chat_message(actor, recipient, content, opts \\ []) do + basic = %{ + "id" => Utils.generate_object_id(), + "actor" => actor.ap_id, + "type" => "ChatMessage", + "to" => [recipient], + "content" => content, + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "emoji" => Emoji.Formatter.get_emoji_map(content) + } + + case opts[:attachment] do + %Object{data: attachment_data} -> + { + :ok, + Map.put(basic, "attachment", attachment_data), + [] + } + + _ -> + {:ok, basic, []} + end + end + @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()} def tombstone(actor, id) do {:ok, diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index a0b3af432..5a4a76085 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do def filter(policies, %{} = object) do policies |> Enum.reduce({:ok, object}, fn - policy, {:ok, object} -> - policy.filter(object) - - _, error -> - error + policy, {:ok, object} -> policy.filter(object) + _, error -> error end) end diff --git a/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex new file mode 100644 index 000000000..8e47f1e02 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/activity_expiration_policy.ex @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do + @moduledoc "Adds expiration to all local Create activities" + @behaviour Pleroma.Web.ActivityPub.MRF + + @impl true + def filter(activity) do + activity = + if note?(activity) and local?(activity) do + maybe_add_expiration(activity) + else + activity + end + + {:ok, activity} + end + + @impl true + def describe, do: {:ok, %{}} + + defp local?(%{"id" => id}) do + String.starts_with?(id, Pleroma.Web.Endpoint.url()) + end + + defp note?(activity) do + match?(%{"type" => "Create", "object" => %{"type" => "Note"}}, activity) + end + + defp maybe_add_expiration(activity) do + days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: days) + + with %{"expires_at" => existing_expires_at} <- activity, + :lt <- NaiveDateTime.compare(existing_expires_at, expires_at) do + activity + else + _ -> Map.put(activity, "expires_at", expires_at) + end + end +end diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 2599067a8..c01c5f780 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator @@ -43,8 +45,20 @@ def validate(%{"type" => "Delete"} = object, meta) do def validate(%{"type" => "Like"} = object, meta) do with {:ok, object} <- - object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do - object = stringify_keys(object |> Map.from_struct()) + object + |> LikeValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object} <- + object + |> ChatMessageValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) {:ok, object, meta} end end @@ -59,6 +73,18 @@ def validate(%{"type" => "EmojiReact"} = object, meta) do end end + def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do + with {:ok, object_data} <- cast_and_apply(object), + meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), + {:ok, create_activity} <- + create_activity + |> CreateChatMessageValidator.cast_and_validate(meta) + |> Ecto.Changeset.apply_action(:insert) do + create_activity = stringify_keys(create_activity) + {:ok, create_activity, meta} + end + end + def validate(%{"type" => "Announce"} = object, meta) do with {:ok, object} <- object @@ -69,17 +95,30 @@ def validate(%{"type" => "Announce"} = object, meta) do end end + def cast_and_apply(%{"type" => "ChatMessage"} = object) do + ChatMessageValidator.cast_and_apply(object) + end + + def cast_and_apply(o), do: {:error, {:validator_not_set, o}} + def stringify_keys(%{__struct__: _} = object) do object |> Map.from_struct() |> stringify_keys end - def stringify_keys(object) do + def stringify_keys(object) when is_map(object) do object - |> Map.new(fn {key, val} -> {to_string(key), val} end) + |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end) end + def stringify_keys(object) when is_list(object) do + object + |> Enum.map(&stringify_keys/1) + end + + def stringify_keys(object), do: object + def fetch_actor(object) do with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do User.get_or_fetch_by_ap_id(actor) diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex new file mode 100644 index 000000000..f53bb02be --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -0,0 +1,80 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field(:type, :string) + field(:mediaType, :string, default: "application/octet-stream") + field(:name, :string) + + embeds_many(:url, UrlObjectValidator) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def changeset(struct, data) do + data = + data + |> fix_media_type() + |> fix_url() + + struct + |> cast(data, [:type, :mediaType, :name]) + |> cast_embed(:url, required: true) + end + + def fix_media_type(data) do + data = + data + |> Map.put_new("mediaType", data["mimeType"]) + + if MIME.valid?(data["mediaType"]) do + data + else + data + |> Map.put("mediaType", "application/octet-stream") + end + end + + def fix_url(data) do + case data["url"] do + url when is_binary(url) -> + data + |> Map.put( + "url", + [ + %{ + "href" => url, + "type" => "Link", + "mediaType" => data["mediaType"] + } + ] + ) + + _ -> + data + end + end + + def validate_data(cng) do + cng + |> validate_required([:mediaType, :url, :type]) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex new file mode 100644 index 000000000..138736f23 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -0,0 +1,123 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do + use Ecto.Schema + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] + + @primary_key false + @derive Jason.Encoder + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:to, Types.Recipients, default: []) + field(:type, :string) + field(:content, Types.SafeText) + field(:actor, Types.ObjectID) + field(:published, Types.DateTime) + field(:emoji, :map, default: %{}) + + embeds_one(:attachment, AttachmentValidator) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def fix(data) do + data + |> fix_emoji() + |> fix_attachment() + |> Map.put_new("actor", data["attributedTo"]) + end + + # Throws everything but the first one away + def fix_attachment(%{"attachment" => [attachment | _]} = data) do + data + |> Map.put("attachment", attachment) + end + + def fix_attachment(data), do: data + + def changeset(struct, data) do + data = fix(data) + + struct + |> cast(data, List.delete(__schema__(:fields), :attachment)) + |> cast_embed(:attachment) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["ChatMessage"]) + |> validate_required([:id, :actor, :to, :type, :published]) + |> validate_content_or_attachment() + |> validate_length(:to, is: 1) + |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit])) + |> validate_local_concern() + end + + def validate_content_or_attachment(cng) do + attachment = get_field(cng, :attachment) + + if attachment do + cng + else + cng + |> validate_required([:content]) + end + end + + @doc """ + Validates the following + - If both users are in our system + - If at least one of the users in this ChatMessage is a local user + - If the recipient is not blocking the actor + """ + def validate_local_concern(cng) do + with actor_ap <- get_field(cng, :actor), + {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, + {_, %User{} = recipient} <- + {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, + {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)}, + {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do + cng + else + {:blocking_actor?, true} -> + cng + |> add_error(:actor, "actor is blocked by recipient") + + {:local?, false} -> + cng + |> add_error(:actor, "actor and recipient are both remote") + + {:find_actor, _} -> + cng + |> add_error(:actor, "can't find user") + + {:find_recipient, _} -> + cng + |> add_error(:to, "can't find user") + end + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex new file mode 100644 index 000000000..fc582400b --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +# NOTES +# - Can probably be a generic create validator +# - doesn't embed, will only get the object id +defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do + use Ecto.Schema + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, Types.ObjectID, primary_key: true) + field(:actor, Types.ObjectID) + field(:type, :string) + field(:to, Types.Recipients, default: []) + field(:object, Types.ObjectID) + end + + def cast_and_apply(data) do + data + |> cast_data + |> apply_action(:insert) + end + + def cast_data(data) do + cast(%__MODULE__{}, data, __schema__(:fields)) + end + + def cast_and_validate(data, meta \\ []) do + cast_data(data) + |> validate_data(meta) + end + + def validate_data(cng, meta \\ []) do + cng + |> validate_required([:id, :actor, :to, :type, :object]) + |> validate_inclusion(:type, ["Create"]) + |> validate_actor_presence() + |> validate_recipients_match(meta) + |> validate_actors_match(meta) + |> validate_object_nonexistence() + end + + def validate_object_nonexistence(cng) do + cng + |> validate_change(:object, fn :object, object_id -> + if Object.get_cached_by_ap_id(object_id) do + [{:object, "The object to create already exists"}] + else + [] + end + end) + end + + def validate_actors_match(cng, meta) do + object_actor = meta[:object_data]["actor"] + + cng + |> validate_change(:actor, fn :actor, actor -> + if actor == object_actor do + [] + else + [{:actor, "Actor doesn't match with object actor"}] + end + end) + end + + def validate_recipients_match(cng, meta) do + object_recipients = meta[:object_data]["to"] || [] + + cng + |> validate_change(:to, fn :to, recipients -> + activity_set = MapSet.new(recipients) + object_set = MapSet.new(object_recipients) + + if MapSet.equal?(activity_set, object_set) do + [] + else + [{:to, "Recipients don't match with object recipients"}] + end + end) + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex similarity index 100% rename from lib/pleroma/web/activity_pub/object_validators/create_validator.ex rename to lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex index f42c03510..e5d08eb5c 100644 --- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex @@ -46,12 +46,13 @@ def add_deleted_activity_id(cng) do Answer Article Audio + ChatMessage Event Note Page Question - Video Tombstone + Video } def validate_data(cng) do cng diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex index 48fe61e1a..408e0f6ee 100644 --- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex +++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex @@ -11,11 +11,13 @@ def cast(object) when is_binary(object) do def cast(data) when is_list(data) do data - |> Enum.reduce({:ok, []}, fn element, acc -> - case {acc, ObjectID.cast(element)} do - {:error, _} -> :error - {_, :error} -> :error - {{:ok, list}, {:ok, id}} -> {:ok, [id | list]} + |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} -> + case ObjectID.cast(element) do + {:ok, id} -> + {:cont, {:ok, [id | list]}} + + _ -> + {:halt, :error} end end) end diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex new file mode 100644 index 000000000..95c948123 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do + use Ecto.Type + + alias Pleroma.HTML + + def type, do: :string + + def cast(str) when is_binary(str) do + {:ok, HTML.filter_tags(str)} + end + + def cast(_), do: :error + + def dump(data) do + {:ok, data} + end + + def load(data) do + {:ok, data} + end +end diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex new file mode 100644 index 000000000..47e231150 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do + use Ecto.Schema + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types + + import Ecto.Changeset + @primary_key false + + embedded_schema do + field(:type, :string) + field(:href, Types.Uri) + field(:mediaType, :string) + end + + def changeset(struct, data) do + struct + |> cast(data, __schema__(:fields)) + |> validate_required([:type, :href, :mediaType]) + end +end diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex index 0c54c4b23..6875c47f6 100644 --- a/lib/pleroma/web/activity_pub/pipeline.ex +++ b/lib/pleroma/web/activity_pub/pipeline.ex @@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} def common_pipeline(object, meta) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do + {:ok, {:ok, activity, meta}} -> + SideEffects.handle_after_transaction(meta) + {:ok, activity, meta} + {:ok, value} -> value diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index fb6275450..1a1cc675c 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do collection, and so on. """ alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.Push + alias Pleroma.Web.Streamer def handle(object, meta \\ []) @@ -27,6 +32,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do {:ok, object, meta} end + # Tasks this handles + # - Actually create object + # - Rollback if we couldn't create it + # - Set up notifications + def handle(%{data: %{"type" => "Create"}} = activity, meta) do + with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do + {:ok, notifications} = Notification.create_notifications(activity, do_send: false) + + meta = + meta + |> add_notifications(notifications) + + {:ok, activity, meta} + else + e -> Repo.rollback(e) + end + end + # Tasks this handles: # - Add announce to object # - Set up notification @@ -88,6 +111,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, Object.decrease_replies_count(in_reply_to) end + MessageReference.delete_for_object(deleted_object) + ActivityPub.stream_out(object) ActivityPub.stream_out_participations(deleted_object, user) :ok @@ -112,6 +137,39 @@ def handle(object, meta) do {:ok, object, meta} end + def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do + with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do + actor = User.get_cached_by_ap_id(object.data["actor"]) + recipient = User.get_cached_by_ap_id(hd(object.data["to"])) + + streamables = + [[actor, recipient], [recipient, actor]] + |> Enum.map(fn [user, other_user] -> + if user.local do + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id) + + { + ["user", "user:pleroma_chat"], + {user, %{cm_ref | chat: chat, object: object}} + } + end + end) + |> Enum.filter(& &1) + + meta = + meta + |> add_streamables(streamables) + + {:ok, object, meta} + end + end + + # Nothing to do + def handle_object_creation(object) do + {:ok, object} + end + def handle_undoing(%{data: %{"type" => "Like"}} = object) do with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), {:ok, _} <- Utils.remove_like_from_object(object, liked_object), @@ -148,4 +206,43 @@ def handle_undoing( end def handle_undoing(object), do: {:error, ["don't know how to handle", object]} + + defp send_notifications(meta) do + Keyword.get(meta, :notifications, []) + |> Enum.each(fn notification -> + Streamer.stream(["user", "user:notification"], notification) + Push.send(notification) + end) + + meta + end + + defp send_streamables(meta) do + Keyword.get(meta, :streamables, []) + |> Enum.each(fn {topics, items} -> + Streamer.stream(topics, items) + end) + + meta + end + + defp add_streamables(meta, streamables) do + existing = Keyword.get(meta, :streamables, []) + + meta + |> Keyword.put(:streamables, streamables ++ existing) + end + + defp add_notifications(meta, notifications) do + existing = Keyword.get(meta, :notifications, []) + + meta + |> Keyword.put(:notifications, notifications ++ existing) + end + + def handle_after_transaction(meta) do + meta + |> send_notifications() + |> send_streamables() + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 8443c284c..985921aa0 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -9,6 +9,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Activity alias Pleroma.EarmarkRenderer alias Pleroma.FollowingRelationship + alias Pleroma.Maps + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -208,12 +210,6 @@ def fix_context(object) do |> Map.put("conversation", context) end - defp add_if_present(map, _key, nil), do: map - - defp add_if_present(map, key, value) do - Map.put(map, key, value) - end - def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do attachments = Enum.map(attachment, fn data -> @@ -226,9 +222,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm media_type = cond do - is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] - is_binary(data["mediaType"]) -> data["mediaType"] - is_binary(data["mimeType"]) -> data["mimeType"] + is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"] + MIME.valid?(data["mediaType"]) -> data["mediaType"] + MIME.valid?(data["mimeType"]) -> data["mimeType"] true -> nil end @@ -241,13 +237,13 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm attachment_url = %{"href" => href} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", Map.get(url || %{}, "type")) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", Map.get(url || %{}, "type")) %{"url" => [attachment_url]} - |> add_if_present("mediaType", media_type) - |> add_if_present("type", data["type"]) - |> add_if_present("name", data["name"]) + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", data["type"]) + |> Maps.put_if_present("name", data["name"]) end) Map.put(object, "attachment", attachments) @@ -532,7 +528,8 @@ def handle_incoming( User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), - {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do + {:ok, activity} <- + ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_locked, User.locked?(followed)}, @@ -575,6 +572,7 @@ def handle_incoming( :noop end + ActivityPub.notify_and_stream(activity) {:ok, activity} else _e -> @@ -595,6 +593,8 @@ def handle_incoming( User.update_follower_count(followed) User.update_following_count(follower) + Notification.update_notification_type(followed, follow_activity) + ActivityPub.accept(%{ to: follow_activity.data["to"], type: "Accept", @@ -662,6 +662,16 @@ def handle_incoming( |> handle_incoming(options) end + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data, + _options + ) do + with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), + {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do + {:ok, activity} + end + end + def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact", "Announce"] do with :ok <- ObjectValidator.fetch_actor_and_object(data), @@ -1113,6 +1123,9 @@ def add_attributed_to(object) do Map.put(object, "attributedTo", attributed_to) end + # TODO: Revisit this + def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object + def prepare_attachments(object) do attachments = object diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index a76a699ee..dfae602df 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Ecto.UUID alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -244,7 +245,7 @@ defp lazy_put_object_defaults(activity, _), do: activity Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) - when is_map(object_data) and type in @supported_object_types do + when type in @supported_object_types do with {:ok, object} <- Object.create(object_data) do map = Map.put(map, "object", object.data["id"]) @@ -307,7 +308,7 @@ def make_like_data( "cc" => cc, "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_emoji_reaction_data(user, object, emoji, activity_id) do @@ -477,7 +478,7 @@ def make_follow_data( "object" => followed_id, "state" => "pending" } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do @@ -546,7 +547,7 @@ def make_announce_data( "cc" => [], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_announce_data( @@ -563,7 +564,7 @@ def make_announce_data( "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end def make_undo_data( @@ -582,7 +583,7 @@ def make_undo_data( "cc" => [Pleroma.Constants.as_public()], "context" => context } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end @spec add_announce_to_object(Activity.t(), Object.t()) :: @@ -627,7 +628,7 @@ def make_unfollow_data(follower, followed, follow_activity, activity_id) do "to" => [followed.ap_id], "object" => follow_activity.data } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Block-related helpers @@ -650,7 +651,7 @@ def make_block_data(blocker, blocked, activity_id) do "to" => [blocked.ap_id], "object" => blocked.ap_id } - |> maybe_put("id", activity_id) + |> Maps.put_if_present("id", activity_id) end #### Create-related helpers @@ -740,13 +741,12 @@ defp build_flag_object(_), do: [] def get_reports(params, page, page_size) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Flag") - |> Map.put("skip_preload", true) - |> Map.put("preload_report_notes", true) - |> Map.put("total", true) - |> Map.put("limit", page_size) - |> Map.put("offset", (page - 1) * page_size) + |> Map.put(:type, "Flag") + |> Map.put(:skip_preload, true) + |> Map.put(:preload_report_notes, true) + |> Map.put(:total, true) + |> Map.put(:limit, page_size) + |> Map.put(:offset, (page - 1) * page_size) ActivityPub.fetch_activities([], params, :offset) end @@ -871,7 +871,4 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> Repo.all() end - - def maybe_put(map, _key, nil), do: map - def maybe_put(map, key, value), do: Map.put(map, key, value) end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 34590b16d..4a02b09a1 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -213,34 +213,24 @@ def render("activity_collection.json", %{iri: iri}) do |> Map.merge(Utils.make_json_ld_header()) end - def render("activity_collection_page.json", %{activities: activities, iri: iri}) do - # this is sorted chronologically, so first activity is the newest (max) - {max_id, min_id, collection} = - if length(activities) > 0 do - { - Enum.at(activities, 0).id, - Enum.at(Enum.reverse(activities), 0).id, - Enum.map(activities, fn act -> - {:ok, data} = Transmogrifier.prepare_outgoing(act.data) - data - end) - } - else - { - 0, - 0, - [] - } - end + def render("activity_collection_page.json", %{ + activities: activities, + iri: iri, + pagination: pagination + }) do + collection = + Enum.map(activities, fn activity -> + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + data + end) %{ - "id" => "#{iri}?max_id=#{max_id}&page=true", "type" => "OrderedCollectionPage", "partOf" => iri, - "orderedItems" => collection, - "next" => "#{iri}?max_id=#{min_id}&page=true" + "orderedItems" => collection } |> Map.merge(Utils.make_json_ld_header()) + |> Map.merge(pagination) end defp maybe_put_total_items(map, false, _total), do: map diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 467d05375..5cbf0dd4f 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do import Pleroma.Web.ControllerHelper, only: [json_response: 3] alias Pleroma.Config - alias Pleroma.ConfigDB alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Plugs.OAuthScopesPlug @@ -17,10 +16,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.AccountView - alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Endpoint @@ -28,7 +25,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do require Logger - @descriptions Pleroma.Docs.JSON.compile() @users_page_size 50 plug( @@ -62,7 +58,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["write:follows"], admin: true} - when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] + when action in [:user_follow, :user_unfollow] ) plug( @@ -75,11 +71,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do OAuthScopesPlug, %{scopes: ["read"], admin: true} when action in [ - :config_show, :list_log, :stats, - :relay_list, - :config_descriptions, :need_reboot ] ) @@ -89,7 +82,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do %{scopes: ["write"], admin: true} when action in [ :restart, - :config_update, :resend_confirmation_email, :confirm_email, :reload_emoji @@ -234,10 +226,10 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do activities = ActivityPub.fetch_statuses(nil, %{ - "instance" => instance, - "limit" => page_size, - "offset" => (page - 1) * page_size, - "exclude_reblogs" => !with_reblogs && "true" + instance: instance, + limit: page_size, + offset: (page - 1) * page_size, + exclude_reblogs: not with_reblogs }) conn @@ -254,9 +246,9 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do activities = ActivityPub.fetch_user_activities(user, nil, %{ - "limit" => page_size, - "godmode" => godmode, - "exclude_reblogs" => !with_reblogs && "true" + limit: page_size, + godmode: godmode, + exclude_reblogs: not with_reblogs }) conn @@ -497,50 +489,6 @@ def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" render_error(conn, :forbidden, "You can't revoke your own admin status.") end - def relay_list(conn, _params) do - with {:ok, list} <- Relay.list() do - json(conn, %{relays: list}) - else - _ -> - conn - |> put_status(500) - end - end - - def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.follow(target) do - ModerationLog.insert_log(%{ - action: "relay_follow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - - def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do - with {:ok, _message} <- Relay.unfollow(target) do - ModerationLog.insert_log(%{ - action: "relay_unfollow", - actor: admin, - target: target - }) - - json(conn, target) - else - _ -> - conn - |> put_status(500) - |> json(target) - end - end - @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_cached_by_nickname(nickname) @@ -645,105 +593,6 @@ def list_log(conn, params) do |> render("index.json", %{log: log}) end - def config_descriptions(conn, _params) do - descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) - - json(conn, descriptions) - end - - def config_show(conn, %{"only_db" => true}) do - with :ok <- configurable_from_database() do - configs = Pleroma.Repo.all(ConfigDB) - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: configs}) - end - end - - def config_show(conn, _params) do - with :ok <- configurable_from_database() do - configs = ConfigDB.get_all_as_keyword() - - merged = - Config.Holder.default_config() - |> ConfigDB.merge(configs) - |> Enum.map(fn {group, values} -> - Enum.map(values, fn {key, value} -> - db = - if configs[group][key] do - ConfigDB.get_db_keys(configs[group][key], key) - end - - db_value = configs[group][key] - - merged_value = - if !is_nil(db_value) and Keyword.keyword?(db_value) and - ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do - ConfigDB.merge_group(group, key, value, db_value) - else - value - end - - setting = %{ - group: ConfigDB.convert(group), - key: ConfigDB.convert(key), - value: ConfigDB.convert(merged_value) - } - - if db, do: Map.put(setting, :db, db), else: setting - end) - end) - |> List.flatten() - - json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - - def config_update(conn, %{"configs" => configs}) do - with :ok <- configurable_from_database() do - {_errors, results} = - configs - |> Enum.filter(&whitelisted_config?/1) - |> Enum.map(fn - %{"group" => group, "key" => key, "delete" => true} = params -> - ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]}) - - %{"group" => group, "key" => key, "value" => value} -> - ConfigDB.update_or_create(%{group: group, key: key, value: value}) - end) - |> Enum.split_with(fn result -> elem(result, 0) == :error end) - - {deleted, updated} = - results - |> Enum.map(fn {:ok, config} -> - Map.put(config, :db, ConfigDB.get_db_keys(config)) - end) - |> Enum.split_with(fn config -> - Ecto.get_meta(config, :state) == :deleted - end) - - Config.TransferTask.load_and_update_env(deleted, false) - - if !Restarter.Pleroma.need_reboot?() do - changed_reboot_settings? = - (updated ++ deleted) - |> Enum.any?(fn config -> - group = ConfigDB.from_string(config.group) - key = ConfigDB.from_string(config.key) - value = ConfigDB.from_binary(config.value) - Config.TransferTask.pleroma_need_restart?(group, key, value) - end) - - if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() - end - - conn - |> put_view(ConfigView) - |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()}) - end - end - def restart(conn, _params) do with :ok <- configurable_from_database() do Restarter.Pleroma.restart(Config.get(:env), 50) @@ -764,28 +613,6 @@ defp configurable_from_database do end end - defp whitelisted_config?(group, key) do - if whitelisted_configs = Config.get(:database_config_whitelist) do - Enum.any?(whitelisted_configs, fn - {whitelisted_group} -> - group == inspect(whitelisted_group) - - {whitelisted_group, whitelisted_key} -> - group == inspect(whitelisted_group) && key == inspect(whitelisted_key) - end) - else - true - end - end - - defp whitelisted_config?(%{"group" => group, "key" => key}) do - whitelisted_config?(group, key) - end - - defp whitelisted_config?(%{:group => group} = config) do - whitelisted_config?(group, config[:key]) - end - def reload_emoji(conn, _params) do Pleroma.Emoji.reload() diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex new file mode 100644 index 000000000..d6e2019bc --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigController do + use Pleroma.Web, :controller + + alias Pleroma.Config + alias Pleroma.ConfigDB + alias Pleroma.Plugs.OAuthScopesPlug + + @descriptions Pleroma.Docs.JSON.compile() + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) + + plug( + OAuthScopesPlug, + %{scopes: ["read"], admin: true} + when action in [:show, :descriptions] + ) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation + + def descriptions(conn, _params) do + descriptions = Enum.filter(@descriptions, &whitelisted_config?/1) + + json(conn, descriptions) + end + + def show(conn, %{only_db: true}) do + with :ok <- configurable_from_database() do + configs = Pleroma.Repo.all(ConfigDB) + render(conn, "index.json", %{configs: configs}) + end + end + + def show(conn, _params) do + with :ok <- configurable_from_database() do + configs = ConfigDB.get_all_as_keyword() + + merged = + Config.Holder.default_config() + |> ConfigDB.merge(configs) + |> Enum.map(fn {group, values} -> + Enum.map(values, fn {key, value} -> + db = + if configs[group][key] do + ConfigDB.get_db_keys(configs[group][key], key) + end + + db_value = configs[group][key] + + merged_value = + if not is_nil(db_value) and Keyword.keyword?(db_value) and + ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do + ConfigDB.merge_group(group, key, value, db_value) + else + value + end + + %{ + group: ConfigDB.convert(group), + key: ConfigDB.convert(key), + value: ConfigDB.convert(merged_value) + } + |> Pleroma.Maps.put_if_present(:db, db) + end) + end) + |> List.flatten() + + json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()}) + end + end + + def update(%{body_params: %{configs: configs}} = conn, _) do + with :ok <- configurable_from_database() do + results = + configs + |> Enum.filter(&whitelisted_config?/1) + |> Enum.map(fn + %{group: group, key: key, delete: true} = params -> + ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]}) + + %{group: group, key: key, value: value} -> + ConfigDB.update_or_create(%{group: group, key: key, value: value}) + end) + |> Enum.reject(fn {result, _} -> result == :error end) + + {deleted, updated} = + results + |> Enum.map(fn {:ok, config} -> + Map.put(config, :db, ConfigDB.get_db_keys(config)) + end) + |> Enum.split_with(fn config -> + Ecto.get_meta(config, :state) == :deleted + end) + + Config.TransferTask.load_and_update_env(deleted, false) + + if not Restarter.Pleroma.need_reboot?() do + changed_reboot_settings? = + (updated ++ deleted) + |> Enum.any?(fn config -> + group = ConfigDB.from_string(config.group) + key = ConfigDB.from_string(config.key) + value = ConfigDB.from_binary(config.value) + Config.TransferTask.pleroma_need_restart?(group, key, value) + end) + + if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot() + end + + render(conn, "index.json", %{ + configs: updated, + need_reboot: Restarter.Pleroma.need_reboot?() + }) + end + end + + defp configurable_from_database do + if Config.get(:configurable_from_database) do + :ok + else + {:error, "To use this endpoint you need to enable configuration from database."} + end + end + + defp whitelisted_config?(group, key) do + if whitelisted_configs = Config.get(:database_config_whitelist) do + Enum.any?(whitelisted_configs, fn + {whitelisted_group} -> + group == inspect(whitelisted_group) + + {whitelisted_group, whitelisted_key} -> + group == inspect(whitelisted_group) && key == inspect(whitelisted_key) + end) + else + true + end + end + + defp whitelisted_config?(%{group: group, key: key}) do + whitelisted_config?(group, key) + end + + defp whitelisted_config?(%{group: group} = config) do + whitelisted_config?(group, config[:key]) + end +end diff --git a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex index 04e629fc1..dca23ea73 100644 --- a/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/oauth_app_controller.ex @@ -42,12 +42,7 @@ def index(conn, params) do end def create(%{body_params: params} = conn, _) do - params = - if params[:name] do - Map.put(params, :client_name, params[:name]) - else - params - end + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) case App.create(params) do {:ok, app} -> @@ -59,12 +54,7 @@ def create(%{body_params: params} = conn, _) do end def update(%{body_params: params} = conn, %{id: id}) do - params = - if params[:name] do - Map.put(params, :client_name, params.name) - else - params - end + params = Pleroma.Maps.put_if_present(params, :client_name, params[:name]) with {:ok, app} <- App.update(id, params) do render(conn, "show.json", app: app, admin: true) diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex new file mode 100644 index 000000000..cf9f3a14b --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/relay_controller.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayController do + use Pleroma.Web, :controller + + alias Pleroma.ModerationLog + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Web.ActivityPub.Relay + + require Logger + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + OAuthScopesPlug, + %{scopes: ["write:follows"], admin: true} + when action in [:follow, :unfollow] + ) + + plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :index) + + action_fallback(Pleroma.Web.AdminAPI.FallbackController) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RelayOperation + + def index(conn, _params) do + with {:ok, list} <- Relay.list() do + json(conn, %{relays: list}) + end + end + + def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end + + def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do + with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + + json(conn, target) + else + _ -> + conn + |> put_status(500) + |> json(target) + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/status_controller.ex b/lib/pleroma/web/admin_api/controllers/status_controller.ex index 574196be8..bc48cc527 100644 --- a/lib/pleroma/web/admin_api/controllers/status_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/status_controller.ex @@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do def index(%{assigns: %{user: _admin}} = conn, params) do activities = ActivityPub.fetch_statuses(nil, %{ - "godmode" => params.godmode, - "local_only" => params.local_only, - "limit" => params.page_size, - "offset" => (params.page - 1) * params.page_size, - "exclude_reblogs" => not params.with_reblogs + godmode: params.godmode, + local_only: params.local_only, + limit: params.page_size, + offset: (params.page - 1) * params.page_size, + exclude_reblogs: not params.with_reblogs }) render(conn, "index.json", activities: activities, as: :activity) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 120159527..e1e929632 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -76,7 +76,8 @@ def render("show.json", %{user: user}) do "local" => user.local, "roles" => User.roles(user), "tags" => user.tags || [], - "confirmation_pending" => user.confirmation_pending + "confirmation_pending" => user.confirmation_pending, + "url" => user.uri || user.ap_id } end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index f432b8c2c..773f798fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -18,7 +18,7 @@ def render("index.json", %{reports: reports}) do %{ reports: reports[:items] - |> Enum.map(&Report.extract_report_info(&1)) + |> Enum.map(&Report.extract_report_info/1) |> Enum.map(&render(__MODULE__, "show.json", &1)) |> Enum.reverse(), total: reports[:total] diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex new file mode 100644 index 000000000..7b38a2ef4 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex @@ -0,0 +1,142 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get list of merged default settings with saved in database", + operationId: "AdminAPI.ConfigController.show", + parameters: [ + Operation.parameter( + :only_db, + :query, + %Schema{type: :boolean, default: false}, + "Get only saved in database settings" + ) + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Update config settings", + operationId: "AdminAPI.ConfigController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any(), + delete: %Schema{type: :boolean}, + subkeys: %Schema{type: :array, items: %Schema{type: :string}} + } + } + } + } + }), + responses: %{ + 200 => Operation.response("Config", "application/json", config_response()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def descriptions_operation do + %Operation{ + tags: ["Admin", "Config"], + summary: "Get JSON with config descriptions.", + operationId: "AdminAPI.ConfigController.descriptions", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Config Descriptions", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + children: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + key: %Schema{type: :string}, + type: %Schema{oneOf: [%Schema{type: :string}, %Schema{type: :array}]}, + description: %Schema{type: :string}, + suggestions: %Schema{type: :array} + } + } + } + } + } + }), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + defp any do + %Schema{ + oneOf: [ + %Schema{type: :array}, + %Schema{type: :object}, + %Schema{type: :string}, + %Schema{type: :integer}, + %Schema{type: :boolean} + ] + } + end + + defp config_response do + %Schema{ + type: :object, + properties: %{ + configs: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + group: %Schema{type: :string}, + key: %Schema{type: :string}, + value: any() + } + } + }, + need_reboot: %Schema{ + type: :boolean, + description: + "If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect" + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex new file mode 100644 index 000000000..7672cb467 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/relay_operation.ex @@ -0,0 +1,83 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.RelayOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "List Relays", + operationId: "AdminAPI.RelayController.index", + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{ + relays: %Schema{ + type: :array, + items: %Schema{type: :string}, + example: ["lain.com", "mstdn.io"] + } + } + }) + } + } + end + + def follow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Follow a Relay", + operationId: "AdminAPI.RelayController.follow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["Admin", "Relays"], + summary: "Unfollow a Relay", + operationId: "AdminAPI.RelayController.unfollow", + security: [%{"oAuth" => ["write:follows"]}], + requestBody: + request_body("Parameters", %Schema{ + type: :object, + properties: %{ + relay_url: %Schema{type: :string, format: :uri} + } + }), + responses: %{ + 200 => + Operation.response("Status", "application/json", %Schema{ + type: :string, + example: "http://mastodon.example.org/users/admin" + }) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex new file mode 100644 index 000000000..cf299bfc2 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -0,0 +1,355 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.ChatOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Chat + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage + + import Pleroma.Web.ApiSpec.Helpers + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def mark_as_read_operation do + %Operation{ + tags: ["chat"], + summary: "Mark all messages in the chat as read", + operationId: "ChatController.mark_as_read", + parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")], + requestBody: request_body("Parameters", mark_as_read()), + responses: %{ + 200 => + Operation.response( + "The updated chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def mark_message_as_read_operation do + %Operation{ + tags: ["chat"], + summary: "Mark one message in the chat as read", + operationId: "ChatController.mark_message_as_read", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], + responses: %{ + 200 => + Operation.response( + "The read ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def show_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + operationId: "ChatController.show", + parameters: [ + Operation.parameter( + :id, + :path, + :string, + "The id of the chat", + required: true, + example: "1234" + ) + ], + responses: %{ + 200 => + Operation.response( + "The existing chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["read"] + } + ] + } + end + + def create_operation do + %Operation{ + tags: ["chat"], + summary: "Create a chat", + operationId: "ChatController.create", + parameters: [ + Operation.parameter( + :id, + :path, + :string, + "The account id of the recipient of this chat", + required: true, + example: "someflakeid" + ) + ], + responses: %{ + 200 => + Operation.response( + "The created or existing chat", + "application/json", + Chat + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def index_operation do + %Operation{ + tags: ["chat"], + summary: "Get a list of chats that you participated in", + operationId: "ChatController.index", + parameters: pagination_params(), + responses: %{ + 200 => Operation.response("The chats of the user", "application/json", chats_response()) + }, + security: [ + %{ + "oAuth" => ["read:chats"] + } + ] + } + end + + def messages_operation do + %Operation{ + tags: ["chat"], + summary: "Get the most recent messages of the chat", + operationId: "ChatController.messages", + parameters: + [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++ + pagination_params(), + responses: %{ + 200 => + Operation.response( + "The messages in the chat", + "application/json", + chat_messages_response() + ) + }, + security: [ + %{ + "oAuth" => ["read:chats"] + } + ] + } + end + + def post_chat_message_operation do + %Operation{ + tags: ["chat"], + summary: "Post a message to the chat", + operationId: "ChatController.post_chat_message", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat") + ], + requestBody: request_body("Parameters", chat_message_create()), + responses: %{ + 200 => + Operation.response( + "The newly created ChatMessage", + "application/json", + ChatMessage + ), + 400 => Operation.response("Bad Request", "application/json", ApiError) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def delete_message_operation do + %Operation{ + tags: ["chat"], + summary: "delete_message", + operationId: "ChatController.delete_message", + parameters: [ + Operation.parameter(:id, :path, :string, "The ID of the Chat"), + Operation.parameter(:message_id, :path, :string, "The ID of the message") + ], + responses: %{ + 200 => + Operation.response( + "The deleted ChatMessage", + "application/json", + ChatMessage + ) + }, + security: [ + %{ + "oAuth" => ["write:chats"] + } + ] + } + end + + def chats_response do + %Schema{ + title: "ChatsResponse", + description: "Response schema for multiple Chats", + type: :array, + items: Chat, + example: [ + %{ + "account" => %{ + "pleroma" => %{ + "is_admin" => false, + "confirmation_pending" => false, + "hide_followers_count" => false, + "is_moderator" => false, + "hide_favorites" => true, + "ap_id" => "https://dontbulling.me/users/lain", + "hide_follows_count" => false, + "hide_follows" => false, + "background_image" => nil, + "skip_thread_containment" => false, + "hide_followers" => false, + "relationship" => %{}, + "tags" => [] + }, + "avatar" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "following_count" => 0, + "header_static" => "https://originalpatchou.li/images/banner.png", + "source" => %{ + "sensitive" => false, + "note" => "lain", + "pleroma" => %{ + "discoverable" => false, + "actor_type" => "Person" + }, + "fields" => [] + }, + "statuses_count" => 1, + "locked" => false, + "created_at" => "2020-04-16T13:40:15.000Z", + "display_name" => "lain", + "fields" => [], + "acct" => "lain@dontbulling.me", + "id" => "9u6Qw6TAZANpqokMkK", + "emojis" => [], + "avatar_static" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "username" => "lain", + "followers_count" => 0, + "header" => "https://originalpatchou.li/images/banner.png", + "bot" => false, + "note" => "lain", + "url" => "https://dontbulling.me/users/lain" + }, + "id" => "1", + "unread" => 2 + } + ] + } + end + + def chat_messages_response do + %Schema{ + title: "ChatMessagesResponse", + description: "Response schema for multiple ChatMessages", + type: :array, + items: ChatMessage, + example: [ + %{ + "emojis" => [ + %{ + "static_url" => "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker" => false, + "shortcode" => "firefox", + "url" => "https://dontbulling.me/emoji/Firefox.gif" + } + ], + "created_at" => "2020-04-21T15:11:46.000Z", + "content" => "Check this out :firefox:", + "id" => "13", + "chat_id" => "1", + "actor_id" => "someflakeid", + "unread" => false + }, + %{ + "actor_id" => "someflakeid", + "content" => "Whats' up?", + "id" => "12", + "chat_id" => "1", + "emojis" => [], + "created_at" => "2020-04-21T15:06:45.000Z", + "unread" => false + } + ] + } + end + + def chat_message_create do + %Schema{ + title: "ChatMessageCreateRequest", + description: "POST body for creating an chat message", + type: :object, + properties: %{ + content: %Schema{ + type: :string, + description: "The content of your message. Optional if media_id is present" + }, + media_id: %Schema{type: :string, description: "The id of an upload"} + }, + example: %{ + "content" => "Hey wanna buy feet pics?", + "media_id" => "134234" + } + } + end + + def mark_as_read do + %Schema{ + title: "MarkAsReadRequest", + description: "POST body for marking a number of chat messages as read", + type: :object, + required: [:last_read_id], + properties: %{ + last_read_id: %Schema{ + type: :string, + description: "The content of your message." + } + }, + example: %{ + "last_read_id" => "abcdef12456" + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index 46e72f8bf..c966b553a 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -185,6 +185,7 @@ defp notification_type do "mention", "poll", "pleroma:emoji_reaction", + "pleroma:chat_mention", "move", "follow_request" ], diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex index c575a87e6..775dd795d 100644 --- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -141,6 +141,11 @@ defp create_request do allOf: [BooleanLike], nullable: true, description: "Receive poll notifications?" + }, + "pleroma:chat_mention": %Schema{ + allOf: [BooleanLike], + nullable: true, + description: "Receive chat notifications?" } } } diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex new file mode 100644 index 000000000..b4986b734 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Chat do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ChatMessage + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Chat", + description: "Response schema for a Chat", + type: :object, + properties: %{ + id: %Schema{type: :string}, + account: %Schema{type: :object}, + unread: %Schema{type: :integer}, + last_message: ChatMessage, + updated_at: %Schema{type: :string, format: :"date-time"} + }, + example: %{ + "account" => %{ + "pleroma" => %{ + "is_admin" => false, + "confirmation_pending" => false, + "hide_followers_count" => false, + "is_moderator" => false, + "hide_favorites" => true, + "ap_id" => "https://dontbulling.me/users/lain", + "hide_follows_count" => false, + "hide_follows" => false, + "background_image" => nil, + "skip_thread_containment" => false, + "hide_followers" => false, + "relationship" => %{}, + "tags" => [] + }, + "avatar" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "following_count" => 0, + "header_static" => "https://originalpatchou.li/images/banner.png", + "source" => %{ + "sensitive" => false, + "note" => "lain", + "pleroma" => %{ + "discoverable" => false, + "actor_type" => "Person" + }, + "fields" => [] + }, + "statuses_count" => 1, + "locked" => false, + "created_at" => "2020-04-16T13:40:15.000Z", + "display_name" => "lain", + "fields" => [], + "acct" => "lain@dontbulling.me", + "id" => "9u6Qw6TAZANpqokMkK", + "emojis" => [], + "avatar_static" => + "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg", + "username" => "lain", + "followers_count" => 0, + "header" => "https://originalpatchou.li/images/banner.png", + "bot" => false, + "note" => "lain", + "url" => "https://dontbulling.me/users/lain" + }, + "id" => "1", + "unread" => 2, + "last_message" => ChatMessage.schema().example(), + "updated_at" => "2020-04-21T15:06:45.000Z" + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex new file mode 100644 index 000000000..3ee85aa76 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "ChatMessage", + description: "Response schema for a ChatMessage", + nullable: true, + type: :object, + properties: %{ + id: %Schema{type: :string}, + account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"}, + chat_id: %Schema{type: :string}, + content: %Schema{type: :string, nullable: true}, + created_at: %Schema{type: :string, format: :"date-time"}, + emojis: %Schema{type: :array}, + attachment: %Schema{type: :object, nullable: true} + }, + example: %{ + "account_id" => "someflakeid", + "chat_id" => "1", + "content" => "hey you again", + "created_at" => "2020-04-21T15:06:45.000Z", + "emojis" => [ + %{ + "static_url" => "https://dontbulling.me/emoji/Firefox.gif", + "visible_in_picker" => false, + "shortcode" => "firefox", + "url" => "https://dontbulling.me/emoji/Firefox.gif" + } + ], + "id" => "14", + "attachment" => nil + } + }) +end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 3f1a50b96..9bcb9f587 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -197,6 +197,13 @@ defp preview?(draft) do defp changes(draft) do direct? = draft.visibility == "direct" + additional = %{"cc" => draft.cc, "directMessage" => direct?} + + additional = + case draft.expires_at do + %NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at) + _ -> additional + end changes = %{ @@ -204,7 +211,7 @@ defp changes(draft) do actor: draft.user, context: draft.context, object: draft.object, - additional: %{"cc" => draft.cc, "directMessage" => direct?} + additional: additional } |> Utils.maybe_add_list_data(draft.user, draft.visibility) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index dbb3d7ade..04e081a8e 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.FollowingRelationship + alias Pleroma.Formatter alias Pleroma.Notification alias Pleroma.Object alias Pleroma.ThreadMute @@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do require Pleroma.Constants require Logger + def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do + with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), + :ok <- validate_chat_content_length(content, !!maybe_attachment), + {_, {:ok, chat_message_data, _meta}} <- + {:build_object, + Builder.chat_message( + user, + recipient.ap_id, + content |> format_chat_content, + attachment: maybe_attachment + )}, + {_, {:ok, create_activity_data, _meta}} <- + {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, + {_, {:ok, %Activity{} = activity, _meta}} <- + {:common_pipeline, + Pipeline.common_pipeline(create_activity_data, + local: true + )} do + {:ok, activity} + end + end + + defp format_chat_content(nil), do: nil + + defp format_chat_content(content) do + {text, _, _} = + content + |> Formatter.html_escape("text/plain") + |> Formatter.linkify() + |> (fn {text, mentions, tags} -> + {String.replace(text, ~r/\r?\n/, "
"), mentions, tags} + end).() + + text + end + + defp validate_chat_content_length(_, true), do: :ok + defp validate_chat_content_length(nil, false), do: {:error, :no_content} + + defp validate_chat_content_length(content, _) do + if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do + :ok + else + {:error, :content_too_long} + end + end + def unblock(blocker, blocked) do with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, {:ok, unblock_data, _} <- Builder.undo(blocker, block), @@ -73,6 +121,7 @@ def accept_follow_request(follower, followed) do object: follow_activity.data["id"], type: "Accept" }) do + Notification.update_notification_type(followed, follow_activity) {:ok, follower} end end @@ -374,20 +423,10 @@ def listen(user, data) do def post(user, %{status: _} = data) do with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do - draft.changes - |> ActivityPub.create(draft.preview?) - |> maybe_create_activity_expiration(draft.expires_at) + ActivityPub.create(draft.changes, draft.preview?) end end - defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do - with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do - {:ok, activity} - end - end - - defp maybe_create_activity_expiration(result, _), do: result - def pin(id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, @@ -427,12 +466,13 @@ def remove_mute(user, activity) do {:ok, activity} end - def thread_muted?(%{id: nil} = _user, _activity), do: false - - def thread_muted?(user, activity) do - ThreadMute.exists?(user.id, activity.data["context"]) + def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}}) + when is_binary("context") do + ThreadMute.exists?(user_id, context) end + def thread_muted?(_, _), do: false + def report(user, data) do with {:ok, account} <- get_reported_account(data.account_id), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 6ec489f9a..15594125f 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -429,7 +429,7 @@ def maybe_notify_mentioned_recipients( %Activity{data: %{"to" => _to, "type" => type} = data} = activity ) when type == "Create" do - object = Object.normalize(activity) + object = Object.normalize(activity, false) object_data = cond do diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 5a1316a5f..5d67d75b5 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ControllerHelper do use Pleroma.Web, :controller + alias Pleroma.Pagination + # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @@ -46,33 +48,8 @@ def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, do: conn def add_link_headers(conn, activities, extra_params) do - case List.last(activities) do - %{id: max_id} -> - params = - conn.params - |> Map.drop(Map.keys(conn.path_params)) - |> Map.drop(["since_id", "max_id", "min_id"]) - |> Map.merge(extra_params) - - limit = - params - |> Map.get("limit", "20") - |> String.to_integer() - - min_id = - if length(activities) <= limit do - activities - |> List.first() - |> Map.get(:id) - else - activities - |> Enum.at(limit * -1) - |> Map.get(:id) - end - - next_url = current_url(conn, Map.merge(params, %{max_id: max_id})) - prev_url = current_url(conn, Map.merge(params, %{min_id: min_id})) - + case get_pagination_fields(conn, activities, extra_params) do + %{"next" => next_url, "prev" => prev_url} -> put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") _ -> @@ -80,9 +57,43 @@ def add_link_headers(conn, activities, extra_params) do end end + def get_pagination_fields(conn, activities, extra_params \\ %{}) do + case List.last(activities) do + %{id: max_id} -> + params = + conn.params + |> Map.drop(Map.keys(conn.path_params)) + |> Map.merge(extra_params) + |> Map.drop(Pagination.page_keys() -- ["limit", "order"]) + + min_id = + activities + |> List.first() + |> Map.get(:id) + + fields = %{ + "next" => current_url(conn, Map.put(params, :max_id, max_id)), + "prev" => current_url(conn, Map.put(params, :min_id, min_id)) + } + + # Generating an `id` without already present pagination keys would + # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id` + # instead of the `q.id > ^min_id` and `q.id < ^max_id`. + # This is because we only have ids present inside of the page, while + # `min_id`, `since_id` and `max_id` requires to know one outside of it. + if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do + Map.put(fields, "id", current_url(conn, conn.params)) + else + fields + end + + _ -> + %{} + end + end + def assign_account_by_id(conn, _) do - # TODO: use `conn.params[:id]` only after moving to OpenAPI - case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do + case Pleroma.User.get_cached_by_id(conn.params.id) do %Pleroma.User{} = account -> assign(conn, :account, account) nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() end @@ -99,11 +110,6 @@ def try_render(conn, _, _) do render_error(conn, :not_implemented, "Can't display this activity") end - @spec put_if_exist(map(), atom() | String.t(), any) :: map() - def put_if_exist(map, _key, nil), do: map - - def put_if_exist(map, key, value), do: Map.put(map, key, value) - @doc """ Returns true if request specifies to include embedded relationships in account objects. May only be used in selected account-related endpoints; has no effect for status- or diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex index 8133f8480..39b2a766a 100644 --- a/lib/pleroma/web/feed/tag_controller.ex +++ b/lib/pleroma/web/feed/tag_controller.ex @@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - def feed(conn, %{"tag" => raw_tag} = params) do {format, tag} = parse_tag(raw_tag) activities = - %{"type" => ["Create"], "tag" => tag} - |> put_if_exist("max_id", params["max_id"]) + %{type: ["Create"], tag: tag} + |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_activities() conn diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 5a6fc9de0..d56f43818 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.Feed.FeedView - import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3] - plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) action_fallback(:errors) @@ -52,10 +50,10 @@ def feed(conn, %{"nickname" => nickname} = params) do with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do activities = %{ - "type" => ["Create"], - "actor_id" => user.ap_id + type: ["Create"], + actor_id: user.ap_id } - |> put_if_exist("max_id", params["max_id"]) + |> Pleroma.Maps.put_if_present(:max_id, params["max_id"]) |> ActivityPub.fetch_public_or_unlisted_activities() conn diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 97295a52f..7cdd8f458 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do json_response: 3 ] + alias Pleroma.Maps alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -160,23 +161,22 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p :discoverable ] |> Enum.reduce(%{}, fn key, acc -> - add_if_present(acc, params, key, key, &{:ok, truthy_param?(&1)}) + Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) end) - |> add_if_present(params, :display_name, :name) - |> add_if_present(params, :note, :bio) - |> add_if_present(params, :avatar, :avatar) - |> add_if_present(params, :header, :banner) - |> add_if_present(params, :pleroma_background_image, :background) - |> add_if_present( - params, - :fields_attributes, + |> Maps.put_if_present(:name, params[:display_name]) + |> Maps.put_if_present(:bio, params[:note]) + |> Maps.put_if_present(:avatar, params[:avatar]) + |> Maps.put_if_present(:banner, params[:header]) + |> Maps.put_if_present(:background, params[:pleroma_background_image]) + |> Maps.put_if_present( :raw_fields, + params[:fields_attributes], &{:ok, normalize_fields_attributes(&1)} ) - |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) - |> add_if_present(params, :default_scope, :default_scope) - |> add_if_present(params["source"], "privacy", :default_scope) - |> add_if_present(params, :actor_type, :actor_type) + |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store]) + |> Maps.put_if_present(:default_scope, params[:default_scope]) + |> Maps.put_if_present(:default_scope, params["source"]["privacy"]) + |> Maps.put_if_present(:actor_type, params[:actor_type]) changeset = User.update_changeset(user, user_params) @@ -206,16 +206,6 @@ defp build_update_activity_params(user) do } end - defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- is_map(params), - true <- Map.has_key?(params, params_field), - {:ok, new_value} <- value_function.(Map.get(params, params_field)) do - Map.put(map, map_field, new_value) - else - _ -> map - end - end - defp normalize_fields_attributes(fields) do if Enum.all?(fields, &is_tuple/1) do Enum.map(fields, fn {_, v} -> v end) @@ -254,9 +244,7 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do params = params |> Map.delete(:tagged) - |> Enum.filter(&(not is_nil(&1))) - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("tag", params[:tagged]) + |> Map.put(:tag, params[:tagged]) activities = ActivityPub.fetch_user_activities(user, reading_user, params) diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index 69f0e3846..f35ec3596 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do - params = stringify_pagination_params(params) participations = Participation.for_user_with_last_activity_id(user, params) conn @@ -37,20 +36,4 @@ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do render(conn, "participation.json", participation: participation, for: user) end end - - defp stringify_pagination_params(params) do - atom_keys = - Pleroma.Pagination.page_keys() - |> Enum.map(&String.to_atom(&1)) - - str_keys = - params - |> Map.take(atom_keys) - |> Enum.map(fn {key, value} -> {to_string(key), value} end) - |> Enum.into(%{}) - - params - |> Map.delete(atom_keys) - |> Map.merge(str_keys) - end end diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index bcd12c73f..e25cef30b 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -42,8 +42,20 @@ def index(conn, %{account_id: account_id} = params) do end end + @default_notification_types ~w{ + mention + follow + follow_request + reblog + favourite + move + pleroma:emoji_reaction + } def index(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {k, v} -> {to_string(k), v} end) + params = + Map.new(params, fn {k, v} -> {to_string(k), v} end) + |> Map.put_new("include_types", @default_notification_types) + notifications = MastodonAPI.get_notifications(user, params) conn diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 8840fc19c..3be0ca095 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -124,6 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do defp prepare_tags(query, add_joined_tag \\ true) do tags = query + |> preprocess_uri_query() |> String.split(~r/[^#\w]+/u, trim: true) |> Enum.uniq_by(&String.downcase/1) @@ -147,6 +148,20 @@ defp prepare_tags(query, add_joined_tag \\ true) do end end + # If `query` is a URI, returns last component of its path, otherwise returns `query` + defp preprocess_uri_query(query) do + if query =~ ~r/https?:\/\// do + query + |> String.trim_trailing("/") + |> URI.parse() + |> Map.get(:path) + |> String.split("/") + |> Enum.at(-1) + else + query + end + end + defp joined_tag(tags) do tags |> Enum.map(fn tag -> String.capitalize(tag) end) diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index f20157a5f..468b44b67 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -359,9 +359,9 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do with %Activity{} = activity <- Activity.get_by_id(id) do activities = ActivityPub.fetch_activities_for_context(activity.data["context"], %{ - "blocking_user" => user, - "user" => user, - "exclude_id" => activity.id + blocking_user: user, + user: user, + exclude_id: activity.id }) render(conn, "context.json", activity: activity, activities: activities, user: user) @@ -370,11 +370,6 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do @doc "GET /api/v1/favourites" def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.take(Pleroma.Pagination.page_keys()) - activities = ActivityPub.fetch_favourites(user, params) conn diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index f67f75430..4bdd46d7e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -44,17 +44,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do def home(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) - - recipients = [user.ap_id | User.following(user)] + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) + |> Map.put(:announce_filtering_user, user) + |> Map.put(:user, user) activities = - recipients + [user.ap_id | User.following(user)] |> ActivityPub.fetch_activities(params) |> Enum.reverse() @@ -71,10 +69,9 @@ def home(%{assigns: %{user: user}} = conn, params) do def direct(%{assigns: %{user: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("blocking_user", user) - |> Map.put("user", user) + |> Map.put(:type, "Create") + |> Map.put(:blocking_user, user) + |> Map.put(:user, user) |> Map.put(:visibility, "direct") activities = @@ -93,9 +90,7 @@ def direct(%{assigns: %{user: user}} = conn, params) do # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - - local_only = params["local"] + local_only = params[:local] cfg_key = if local_only do @@ -111,11 +106,11 @@ def public(%{assigns: %{user: user}} = conn, params) do else activities = params - |> Map.put("type", ["Create"]) - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create"]) + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() conn @@ -130,39 +125,38 @@ def public(%{assigns: %{user: user}} = conn, params) do defp hashtag_fetching(params, user, local_only) do tags = - [params["tag"], params["any"]] + [params[:tag], params[:any]] |> List.flatten() |> Enum.uniq() - |> Enum.filter(& &1) - |> Enum.map(&String.downcase(&1)) + |> Enum.reject(&is_nil/1) + |> Enum.map(&String.downcase/1) tag_all = params - |> Map.get("all", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:all, []) + |> Enum.map(&String.downcase/1) tag_reject = params - |> Map.get("none", []) - |> Enum.map(&String.downcase(&1)) + |> Map.get(:none, []) + |> Enum.map(&String.downcase/1) _activities = params - |> Map.put("type", "Create") - |> Map.put("local_only", local_only) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("tag", tags) - |> Map.put("tag_all", tag_all) - |> Map.put("tag_reject", tag_reject) + |> Map.put(:type, "Create") + |> Map.put(:local_only, local_only) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:tag, tags) + |> Map.put(:tag_all, tag_all) + |> Map.put(:tag_reject, tag_reject) |> ActivityPub.fetch_public_activities() end # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do - params = Map.new(params, fn {key, value} -> {to_string(key), value} end) - local_only = params["local"] + local_only = params[:local] activities = hashtag_fetching(params, user, local_only) conn diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 70da64a7a..694bf5ca8 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do import Ecto.Query import Ecto.Changeset - alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.Pagination alias Pleroma.ScheduledActivity @@ -82,15 +81,11 @@ defp cast_params(params) do end defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do - ap_types = convert_and_filter_mastodon_types(mastodon_types) - - where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) + where(query, [n], n.type in ^mastodon_types) end defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do - ap_types = convert_and_filter_mastodon_types(mastodon_types) - - where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data)) + where(query, [n], n.type not in ^mastodon_types) end defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do @@ -98,10 +93,4 @@ defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do end defp restrict(query, _, _), do: query - - defp convert_and_filter_mastodon_types(types) do - types - |> Enum.map(&Activity.from_mastodon_notification_type/1) - |> Enum.filter(& &1) - end end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 04c419d2f..9fc06bf9d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -235,6 +235,7 @@ defp do_render("show.json", %{user: user} = opts) do # Pleroma extension pleroma: %{ + ap_id: user.ap_id, confirmation_pending: user.confirmation_pending, tags: user.tags, hide_followers_count: user.hide_followers_count, diff --git a/lib/pleroma/web/mastodon_api/views/app_view.ex b/lib/pleroma/web/mastodon_api/views/app_view.ex index 36071cd25..e44272c6f 100644 --- a/lib/pleroma/web/mastodon_api/views/app_view.ex +++ b/lib/pleroma/web/mastodon_api/views/app_view.ex @@ -45,10 +45,6 @@ def render("short.json", %{app: %App{website: webiste, client_name: name}}) do defp with_vapid_key(data) do vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] - if vapid_key do - Map.put(data, "vapid_key", vapid_key) - else - data - end + Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key) end end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 2b6f84c72..fbe618377 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -24,8 +24,8 @@ def render("participation.json", %{participation: participation, for: user}) do last_activity_id = with nil <- participation.last_activity_id do ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ - "user" => user, - "blocking_user" => user + user: user, + blocking_user: user }) end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 6a630eafa..c498fe632 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -69,7 +69,8 @@ def features do if Config.get([:instance, :safe_dm_mentions]) do "safe_dm_mentions" end, - "pleroma_emoji_reactions" + "pleroma_emoji_reactions", + "pleroma_chat_messages" ] |> Enum.filter(& &1) end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index c46ddcf55..b11578623 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.Chat.MessageReference alias Pleroma.Notification + alias Pleroma.Object alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + @parent_types ~w{Like Announce EmojiReact} def render("index.json", %{notifications: notifications, for: reading_user} = opts) do activities = Enum.map(notifications, & &1.activity) parent_activities = activities - |> Enum.filter( - &(Activity.mastodon_notification_type(&1) in [ - "favourite", - "reblog", - "pleroma:emoji_reaction" - ]) - ) + |> Enum.filter(fn + %{data: %{"type" => type}} -> + type in @parent_types + end) |> Enum.map(& &1.data["object"]) |> Activity.create_by_object_ap_id() |> Activity.with_preloaded_object(:left) @@ -42,7 +44,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op true -> move_activities_targets = activities - |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move")) + |> Enum.filter(&(&1.data["type"] == "Move")) |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"])) actors = @@ -79,8 +81,6 @@ def render( end end - mastodon_type = Activity.mastodon_notification_type(activity) - # Note: :relationships contain user mutes (needed for :muted flag in :status) status_render_opts = %{relationships: opts[:relationships]} @@ -91,7 +91,7 @@ def render( ) do response = %{ id: to_string(notification.id), - type: mastodon_type, + type: notification.type, created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), account: account, pleroma: %{ @@ -99,7 +99,7 @@ def render( } } - case mastodon_type do + case notification.type do "mention" -> put_status(response, activity, reading_user, status_render_opts) @@ -117,6 +117,9 @@ def render( |> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_emoji(activity) + "pleroma:chat_mention" -> + put_chat_message(response, activity, reading_user, status_render_opts) + type when type in ["follow", "follow_request"] -> response @@ -132,6 +135,17 @@ defp put_emoji(response, activity) do Map.put(response, :emoji, activity.data["content"]) end + defp put_chat_message(response, activity, reading_user, opts) do + object = Object.normalize(activity) + author = User.get_cached_by_ap_id(object.data["actor"]) + chat = Pleroma.Chat.get(reading_user.id, author.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref}) + chat_message_render = MessageReferenceView.render("show.json", render_opts) + + Map.put(response, :chat_message, chat_message_render) + end + defp put_status(response, activity, reading_user, opts) do status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user}) status_render = StatusView.render("show.json", status_render_opts) diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex index 458f6bc78..5b896bf3b 100644 --- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex +++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex @@ -30,7 +30,7 @@ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attac defp with_media_attachments(data, _), do: data defp status_params(params) do - data = %{ + %{ text: params["status"], sensitive: params["sensitive"], spoiler_text: params["spoiler_text"], @@ -39,10 +39,6 @@ defp status_params(params) do poll: params["poll"], in_reply_to_id: params["in_reply_to_id"] } - - case params["media_ids"] do - nil -> data - media_ids -> Map.put(data, :media_ids, media_ids) - end + |> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"]) end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 7c804233c..c557778ca 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller alias Pleroma.Helpers.UriHelper + alias Pleroma.Maps alias Pleroma.MFA alias Pleroma.Plugs.RateLimiter alias Pleroma.Registration @@ -108,7 +109,7 @@ defp handle_existing_authorization( if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{access_token: token.token} - url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) + url_params = Maps.put_if_present(url_params, :state, params["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else @@ -147,7 +148,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ if redirect_uri in String.split(app.redirect_uris) do redirect_uri = redirect_uri(conn, redirect_uri) url_params = %{code: auth.token} - url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) + url_params = Maps.put_if_present(url_params, :state, auth_attrs["state"]) url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) else diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 0a3f45620..f3554d919 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -126,10 +126,9 @@ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", "Create") - |> Map.put("favorited_by", user.ap_id) - |> Map.put("blocking_user", for_user) + |> Map.put(:type, "Create") + |> Map.put(:favorited_by, user.ap_id) + |> Map.put(:blocking_user, for_user) recipients = if for_user do diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex new file mode 100644 index 000000000..c8ef3d915 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -0,0 +1,174 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Pagination + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView + + import Ecto.Query + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + plug( + OAuthScopesPlug, + %{scopes: ["write:chats"]} + when action in [ + :post_chat_message, + :create, + :mark_as_read, + :mark_message_as_read, + :delete_message + ] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["read:chats"]} when action in [:messages, :index, :show] + ) + + plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation + + def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ + message_id: message_id, + id: chat_id + }) do + with %MessageReference{} = cm_ref <- + MessageReference.get_by_id(message_id), + ^chat_id <- cm_ref.chat_id |> to_string(), + %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), + {:ok, _} <- remove_or_delete(cm_ref, user) do + conn + |> put_view(MessageReferenceView) + |> render("show.json", chat_message_reference: cm_ref) + else + _e -> + {:error, :could_not_delete} + end + end + + defp remove_or_delete( + %{object: %{data: %{"actor" => actor, "id" => id}}}, + %{ap_id: actor} = user + ) do + with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do + CommonAPI.delete(activity.id, user) + end + end + + defp remove_or_delete(cm_ref, _) do + cm_ref + |> MessageReference.delete() + end + + def post_chat_message( + %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn, + %{ + id: id + } + ) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), + {:ok, activity} <- + CommonAPI.post_chat_message(user, recipient, params[:content], + media_id: params[:media_id] + ), + message <- Object.normalize(activity, false), + cm_ref <- MessageReference.for_chat_and_object(chat, message) do + conn + |> put_view(MessageReferenceView) + |> render("show.json", for: user, chat_message_reference: cm_ref) + end + end + + def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{ + id: chat_id, + message_id: message_id + }) do + with %MessageReference{} = cm_ref <- + MessageReference.get_by_id(message_id), + ^chat_id <- cm_ref.chat_id |> to_string(), + %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id), + {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do + conn + |> put_view(MessageReferenceView) + |> render("show.json", for: user, chat_message_reference: cm_ref) + end + end + + def mark_as_read( + %{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn, + %{id: id} + ) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), + {_n, _} <- + MessageReference.set_all_seen_for_chat(chat, last_read_id) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end + + def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do + with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do + cm_refs = + chat + |> MessageReference.for_chat_query() + |> Pagination.fetch_paginated(params) + + conn + |> put_view(MessageReferenceView) + |> render("index.json", for: user, chat_message_references: cm_refs) + else + _ -> + conn + |> put_status(:not_found) + |> json(%{error: "not found"}) + end + end + + def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do + blocked_ap_ids = User.blocked_users_ap_ids(user) + + chats = + from(c in Chat, + where: c.user_id == ^user_id, + where: c.recipient not in ^blocked_ap_ids, + order_by: [desc: c.updated_at] + ) + |> Repo.all() + + conn + |> put_view(ChatView) + |> render("index.json", chats: chats) + end + + def create(%{assigns: %{user: user}} = conn, params) do + with %User{ap_id: recipient} <- User.get_by_id(params[:id]), + {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end + + def show(%{assigns: %{user: user}} = conn, params) do + with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do + conn + |> put_view(ChatView) + |> render("show.json", chat: chat) + end + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex index 21d5eb8d5..3d007f324 100644 --- a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex @@ -42,15 +42,14 @@ def statuses( Participation.get(participation_id, preload: [:conversation]) do params = params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities = participation.conversation.ap_id |> ActivityPub.fetch_activities_for_context_query(params) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false)) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false)) |> Enum.reverse() conn diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 8665ca56c..e9a4fba92 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -36,10 +36,7 @@ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do - params = - params - |> Map.new(fn {key, value} -> {to_string(key), value} end) - |> Map.put("type", ["Listen"]) + params = Map.put(params, :type, ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex new file mode 100644 index 000000000..f2112a86e --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do + use Pleroma.Web, :view + + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.StatusView + + def render( + "show.json", + %{ + chat_message_reference: %{ + id: id, + object: %{data: chat_message}, + chat_id: chat_id, + unread: unread + } + } + ) do + %{ + id: id |> to_string(), + content: chat_message["content"], + chat_id: chat_id |> to_string(), + account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, + created_at: Utils.to_masto_date(chat_message["published"]), + emojis: StatusView.build_emojis(chat_message["emoji"]), + attachment: + chat_message["attachment"] && + StatusView.render("attachment.json", attachment: chat_message["attachment"]), + unread: unread + } + end + + def render("index.json", opts) do + render_many( + opts[:chat_message_references], + __MODULE__, + "show.json", + Map.put(opts, :as, :chat_message_reference) + ) + end +end diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex new file mode 100644 index 000000000..1c996da11 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatView do + use Pleroma.Web, :view + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.User + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + def render("show.json", %{chat: %Chat{} = chat} = opts) do + recipient = User.get_cached_by_ap_id(chat.recipient) + last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat) + + %{ + id: chat.id |> to_string(), + account: AccountView.render("show.json", Map.put(opts, :user, recipient)), + unread: MessageReference.unread_count_for_chat(chat), + last_message: + last_message && + MessageReferenceView.render("show.json", chat_message_reference: last_message), + updated_at: Utils.to_masto_date(chat.updated_at) + } + end + + def render("index.json", %{chats: chats}) do + render_many(chats, __MODULE__, "show.json") + end +end diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 691725702..cdb827e76 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do require Logger import Ecto.Query - defdelegate mastodon_notification_type(activity), to: Activity - @types ["Create", "Follow", "Announce", "Like", "Move"] @doc "Performs sending notifications for user subscriptions" @@ -31,10 +29,10 @@ def perform( when activity_type in @types do actor = User.get_cached_by_ap_id(notification.activity.data["actor"]) - mastodon_type = mastodon_notification_type(notification.activity) + mastodon_type = notification.type gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) - object = Object.normalize(activity) + object = Object.normalize(activity, false) user = User.get_cached_by_id(user_id) direct_conversation_id = Activity.direct_conversation_id(activity, user) @@ -116,7 +114,7 @@ def build_content( end def build_content(notification, actor, object, mastodon_type) do - mastodon_type = mastodon_type || mastodon_notification_type(notification.activity) + mastodon_type = mastodon_type || notification.type %{ title: format_title(notification, mastodon_type), @@ -126,6 +124,13 @@ def build_content(notification, actor, object, mastodon_type) do def format_body(activity, actor, object, mastodon_type \\ nil) + def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do + case content do + nil -> "@#{actor.nickname}: (Attachment)" + content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" + end + end + def format_body( %{activity: %{data: %{"type" => "Create"}}}, actor, @@ -151,7 +156,7 @@ def format_body( mastodon_type ) when type in ["Follow", "Like"] do - mastodon_type = mastodon_type || mastodon_notification_type(notification.activity) + mastodon_type = mastodon_type || notification.type case mastodon_type do "follow" -> "@#{actor.nickname} has followed you" @@ -166,15 +171,14 @@ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_typ "New Direct Message" end - def format_title(%{activity: activity}, mastodon_type) do - mastodon_type = mastodon_type || mastodon_notification_type(activity) - - case mastodon_type do + def format_title(%{type: type}, mastodon_type) do + case mastodon_type || type do "mention" -> "New Mention" "follow" -> "New Follower" "follow_request" -> "New Follow Request" "reblog" -> "New Repeat" "favourite" -> "New Favorite" + "pleroma:chat_mention" -> "New Chat Message" type -> "New #{String.capitalize(type || "event")}" end end diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 3e401a490..5b5aa0d59 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do timestamps() end - @supported_alert_types ~w[follow favourite mention reblog]a + @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a defp alerts(%{data: %{alerts: alerts}}) do alerts = Map.take(alerts, @supported_alert_types) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3b55afede..83f74455d 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -160,9 +160,9 @@ defmodule Pleroma.Web.Router do :right_delete_multiple ) - get("/relay", AdminAPIController, :relay_list) - post("/relay", AdminAPIController, :relay_follow) - delete("/relay", AdminAPIController, :relay_unfollow) + get("/relay", RelayController, :index) + post("/relay", RelayController, :follow) + delete("/relay", RelayController, :unfollow) post("/users/invite_token", InviteController, :create) get("/users/invites", InviteController, :index) @@ -194,9 +194,9 @@ defmodule Pleroma.Web.Router do delete("/statuses/:id", StatusController, :delete) get("/statuses", StatusController, :index) - get("/config", AdminAPIController, :config_show) - post("/config", AdminAPIController, :config_update) - get("/config/descriptions", AdminAPIController, :config_descriptions) + get("/config", ConfigController, :show) + post("/config", ConfigController, :update) + get("/config/descriptions", ConfigController, :descriptions) get("/need_reboot", AdminAPIController, :need_reboot) get("/restart", AdminAPIController, :restart) @@ -306,6 +306,15 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:authenticated_api) + post("/chats/by-account-id/:id", ChatController, :create) + get("/chats", ChatController, :index) + get("/chats/:id", ChatController, :show) + get("/chats/:id/messages", ChatController, :messages) + post("/chats/:id/messages", ChatController, :post_chat_message) + delete("/chats/:id/messages/:message_id", ChatController, :delete_message) + post("/chats/:id/read", ChatController, :mark_as_read) + post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read) + get("/conversations/:id/statuses", ConversationController, :statuses) get("/conversations/:id", ConversationController, :show) post("/conversations/read", ConversationController, :mark_as_read) @@ -571,13 +580,6 @@ defmodule Pleroma.Web.Router do get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end - scope "/", Pleroma.Web.ActivityPub do - # XXX: not really ostatus - pipe_through(:ostatus) - - get("/users/:nickname/outbox", ActivityPubController, :outbox) - end - pipeline :ap_service_actor do plug(:accepts, ["activity+json", "json"]) end @@ -602,6 +604,7 @@ defmodule Pleroma.Web.Router do get("/api/ap/whoami", ActivityPubController, :whoami) get("/users/:nickname/inbox", ActivityPubController, :read_inbox) + get("/users/:nickname/outbox", ActivityPubController, :outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index c3efb6651..a7a891b13 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -111,8 +111,14 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do %User{} = user -> meta = Metadata.build_tags(%{user: user}) + params = + params + |> Map.take(@page_keys) + |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + timeline = - ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys)) + user + |> ActivityPub.fetch_user_activities(nil, params) |> Enum.map(&represent/1) prev_page_id = diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 0cf41189b..d1d2c9b9c 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do require Logger alias Pleroma.Activity + alias Pleroma.Chat.MessageReference alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Notification @@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do def registry, do: @registry @public_streams ["public", "public:local", "public:media", "public:local:media"] - @user_streams ["user", "user:notification", "direct"] + @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"] @doc "Expands and authorizes a stream, and registers the process for streaming." @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) :: @@ -89,34 +90,20 @@ def remove_socket(topic) do if should_env_send?(), do: Registry.unregister(@registry, topic) end - def stream(topics, item) when is_list(topics) do + def stream(topics, items) do if should_env_send?() do - Enum.each(topics, fn t -> - spawn(fn -> do_stream(t, item) end) + List.wrap(topics) + |> Enum.each(fn topic -> + List.wrap(items) + |> Enum.each(fn item -> + spawn(fn -> do_stream(topic, item) end) + end) end) end :ok end - def stream(topic, items) when is_list(items) do - if should_env_send?() do - Enum.each(items, fn i -> - spawn(fn -> do_stream(topic, i) end) - end) - - :ok - end - end - - def stream(topic, item) do - if should_env_send?() do - spawn(fn -> do_stream(topic, item) end) - end - - :ok - end - def filtered_by_user?(%User{} = user, %Activity{} = item) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) @@ -200,6 +187,19 @@ defp do_stream(topic, %Notification{} = item) end) end + defp do_stream(topic, {user, %MessageReference{} = cm_ref}) + when topic in ["user", "user:pleroma_chat"] do + topic = "#{topic}:#{user.id}" + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + Registry.dispatch(@registry, topic, fn list -> + Enum.each(list, fn {pid, _auth} -> + send(pid, {:text, text}) + end) + end) + end + defp do_stream("user", item) do Logger.debug("Trying to push to users") diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex index 237b29ded..476a33245 100644 --- a/lib/pleroma/web/views/streamer_view.ex +++ b/lib/pleroma/web/views/streamer_view.ex @@ -51,6 +51,29 @@ def render("update.json", %Activity{} = activity) do |> Jason.encode!() end + def render("chat_update.json", %{chat_message_reference: cm_ref}) do + # Explicitly giving the cmr for the object here, so we don't accidentally + # send a later 'last_message' that was inserted between inserting this and + # streaming it out + # + # It also contains the chat with a cache of the correct unread count + Logger.debug("Trying to stream out #{inspect(cm_ref)}") + + representation = + Pleroma.Web.PleromaAPI.ChatView.render( + "show.json", + %{last_message: cm_ref, chat: cm_ref.chat} + ) + + %{ + event: "pleroma:chat_update", + payload: + representation + |> Jason.encode!() + } + |> Jason.encode!() + end + def render("conversation.json", %Participation{} = participation) do %{ event: "conversation", diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs new file mode 100644 index 000000000..715d798ea --- /dev/null +++ b/priv/repo/migrations/20200309123730_create_chats.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.CreateChats do + use Ecto.Migration + + def change do + create table(:chats) do + add(:user_id, references(:users, type: :uuid)) + # Recipient is an ActivityPub id, to future-proof for group support. + add(:recipient, :string) + add(:unread, :integer, default: 0) + timestamps() + end + + # There's only one chat between a user and a recipient. + create(index(:chats, [:user_id, :recipient], unique: true)) + end +end diff --git a/priv/repo/migrations/20200602094828_add_type_to_notifications.exs b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs new file mode 100644 index 000000000..19c733628 --- /dev/null +++ b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddTypeToNotifications do + use Ecto.Migration + + def change do + alter table(:notifications) do + add(:type, :string) + end + end +end diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs new file mode 100644 index 000000000..996d721ee --- /dev/null +++ b/priv/repo/migrations/20200602125218_backfill_notification_types.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do + use Ecto.Migration + + def up do + Pleroma.MigrationHelper.NotificationBackfill.fill_in_notification_types() + end + + def down do + end +end diff --git a/priv/repo/migrations/20200602150528_create_chat_message_reference.exs b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs new file mode 100644 index 000000000..6f9148b7c --- /dev/null +++ b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do + use Ecto.Migration + + def change do + create table(:chat_message_references, primary_key: false) do + add(:id, :uuid, primary_key: true) + add(:chat_id, references(:chats, on_delete: :delete_all), null: false) + add(:object_id, references(:objects, on_delete: :delete_all), null: false) + add(:seen, :boolean, default: false, null: false) + + timestamps() + end + + create(index(:chat_message_references, [:chat_id, "id desc"])) + end +end diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs new file mode 100644 index 000000000..fdf85132e --- /dev/null +++ b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddUniqueIndexToChatMessageReferences do + use Ecto.Migration + + def change do + create(unique_index(:chat_message_references, [:object_id, :chat_id])) + end +end diff --git a/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs new file mode 100644 index 000000000..6322137d5 --- /dev/null +++ b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.RemoveUnreadFromChats do + use Ecto.Migration + + def change do + alter table(:chats) do + remove(:unread, :integer, default: 0) + end + end +end diff --git a/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs new file mode 100644 index 000000000..a5065d612 --- /dev/null +++ b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddSeenIndexToChatMessageReferences do + use Ecto.Migration + + def change do + create( + index(:chat_message_references, [:chat_id], + where: "seen = false", + name: "unseen_messages_count_index" + ) + ) + end +end diff --git a/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs new file mode 100644 index 000000000..fd6bc7bc7 --- /dev/null +++ b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs @@ -0,0 +1,30 @@ +defmodule Pleroma.Repo.Migrations.MigrateSeenToUnreadInChatMessageReferences do + use Ecto.Migration + + def change do + drop( + index(:chat_message_references, [:chat_id], + where: "seen = false", + name: "unseen_messages_count_index" + ) + ) + + alter table(:chat_message_references) do + add(:unread, :boolean, default: true) + end + + execute("update chat_message_references set unread = not seen") + + alter table(:chat_message_references) do + modify(:unread, :boolean, default: true, null: false) + remove(:seen, :boolean, default: false, null: false) + end + + create( + index(:chat_message_references, [:chat_id], + where: "unread = true", + name: "unread_messages_count_index" + ) + ) + end +end diff --git a/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs new file mode 100644 index 000000000..9ea34436b --- /dev/null +++ b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs @@ -0,0 +1,36 @@ +defmodule Pleroma.Repo.Migrations.ChangeTypeToEnumForNotifications do + use Ecto.Migration + + def up do + """ + create type notification_type as enum ( + 'follow', + 'follow_request', + 'mention', + 'move', + 'pleroma:emoji_reaction', + 'pleroma:chat_mention', + 'reblog', + 'favourite' + ) + """ + |> execute() + + """ + alter table notifications + alter column type type notification_type using (type::notification_type) + """ + |> execute() + end + + def down do + alter table(:notifications) do + modify(:type, :string) + end + + """ + drop type notification_type + """ + |> execute() + end +end diff --git a/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs new file mode 100644 index 000000000..f14e269ca --- /dev/null +++ b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs @@ -0,0 +1,23 @@ +defmodule Pleroma.Repo.Migrations.ChangeChatIdToFlake do + use Ecto.Migration + + def up do + execute(""" + alter table chats + drop constraint chats_pkey cascade, + alter column id drop default, + alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid), + add primary key (id) + """) + + execute(""" + alter table chat_message_references + alter column chat_id set data type uuid using cast( lpad( to_hex(chat_id), 32, '0') as uuid), + add constraint chat_message_references_chat_id_fkey foreign key (chat_id) references chats(id) on delete cascade + """) + end + + def down do + :ok + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 278ad2f96..7cc3fee40 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -30,6 +30,7 @@ "@type": "@id" }, "EmojiReact": "litepub:EmojiReact", + "ChatMessage": "litepub:ChatMessage", "alsoKnownAs": { "@id": "as:alsoKnownAs", "@type": "@id" diff --git a/test/chat/message_reference_test.exs b/test/chat/message_reference_test.exs new file mode 100644 index 000000000..aaa7c1ad4 --- /dev/null +++ b/test/chat/message_reference_test.exs @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Chat.MessageReferenceTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "messages" do + test "it returns the last message in a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey") + {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho") + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + message = MessageReference.last_message_for_chat(chat) + + assert message.object.data["content"] == "ho" + end + end +end diff --git a/test/chat_test.exs b/test/chat_test.exs new file mode 100644 index 000000000..332f2180a --- /dev/null +++ b/test/chat_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ChatTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Chat + + import Pleroma.Factory + + describe "creation and getting" do + test "it only works if the recipient is a valid user (for now)" do + user = insert(:user) + + assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account") + assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account") + end + + test "it creates a chat for a user and recipient" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id + end + + test "it returns and bumps a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "it returns a chat for a user and recipient if it already exists" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + {:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + end + + test "a returning chat will have an updated `update_at` field" do + user = insert(:user) + other_user = insert(:user) + + {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) + :timer.sleep(1500) + {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id) + + assert chat.id == chat_two.id + assert chat.updated_at != chat_two.updated_at + end + end +end diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json new file mode 100644 index 000000000..9c23a1c9b --- /dev/null +++ b/test/fixtures/create-chat-message.json @@ -0,0 +1,31 @@ +{ + "actor": "http://2hu.gensokyo/users/raymoo", + "id": "http://2hu.gensokyo/objects/1", + "object": { + "attributedTo": "http://2hu.gensokyo/users/raymoo", + "content": "You expected a cute girl? Too bad. ", + "id": "http://2hu.gensokyo/objects/2", + "published": "2020-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "tag": [ + { + "icon": { + "type": "Image", + "url": "http://2hu.gensokyo/emoji/Firefox.gif" + }, + "id": "http://2hu.gensokyo/emoji/Firefox.gif", + "name": ":firefox:", + "type": "Emoji", + "updated": "1970-01-01T00:00:00Z" + } + ], + "type": "ChatMessage" + }, + "published": "2018-02-12T14:08:20Z", + "to": [ + "http://2hu.gensokyo/users/marisa" + ], + "type": "Create" +} diff --git a/test/migration_helper/notification_backfill_test.exs b/test/migration_helper/notification_backfill_test.exs new file mode 100644 index 000000000..2a62a2b00 --- /dev/null +++ b/test/migration_helper/notification_backfill_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MigrationHelper.NotificationBackfillTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.MigrationHelper.NotificationBackfill + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "fill_in_notification_types" do + test "it fills in missing notification types" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) + {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo") + {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + {:ok, like} = CommonAPI.favorite(other_user, post.id) + {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕") + + data = + react_2.data + |> Map.put("type", "EmojiReaction") + + {:ok, react_2} = + react_2 + |> Activity.change(%{data: data}) + |> Repo.update() + + assert {5, nil} = Repo.update_all(Notification, set: [type: nil]) + + NotificationBackfill.fill_in_notification_types() + + assert %{type: "mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id) + + assert %{type: "favourite"} = + Repo.get_by(Notification, user_id: user.id, activity_id: like.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react.id) + + assert %{type: "pleroma:emoji_reaction"} = + Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) + + assert %{type: "pleroma:chat_mention"} = + Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id) + end + end +end diff --git a/test/notification_test.exs b/test/notification_test.exs index 37c255fee..b9bbdceca 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.NotificationTest do alias Pleroma.FollowingRelationship alias Pleroma.Notification + alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -31,6 +32,7 @@ test "creates a notification for an emoji reaction" do {:ok, [notification]} = Notification.create_notifications(activity) assert notification.user_id == user.id + assert notification.type == "pleroma:emoji_reaction" end test "notifies someone when they are directly addressed" do @@ -48,6 +50,7 @@ test "notifies someone when they are directly addressed" do notified_ids = Enum.sort([notification.user_id, other_notification.user_id]) assert notified_ids == [other_user.id, third_user.id] assert notification.activity_id == activity.id + assert notification.type == "mention" assert other_notification.activity_id == activity.id assert [%Pleroma.Marker{unread_count: 2}] = @@ -335,9 +338,12 @@ test "it creates `follow_request` notification for pending Follow activity" do # After request is accepted, the same notification is rendered with type "follow": assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) - notification_id = notification.id - assert [%{id: ^notification_id}] = Notification.for_user(followed_user) - assert %{type: "follow"} = NotificationView.render("show.json", render_opts) + notification = + Repo.get(Notification, notification.id) + |> Repo.preload(:activity) + + assert %{type: "follow"} = + NotificationView.render("show.json", notification: notification, for: followed_user) end test "it doesn't create a notification for follow-unfollow-follow chains" do diff --git a/test/pagination_test.exs b/test/pagination_test.exs index d5b1b782d..9165427ae 100644 --- a/test/pagination_test.exs +++ b/test/pagination_test.exs @@ -21,7 +21,7 @@ test "paginates by min_id", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"min_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{min_id: id, total: true}) assert length(paginated) == 2 assert total == 5 @@ -31,7 +31,7 @@ test "paginates by since_id", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"since_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{since_id: id, total: true}) assert length(paginated) == 2 assert total == 5 @@ -41,7 +41,7 @@ test "paginates by max_id", %{notes: notes} do id = Enum.at(notes, 1).id |> Integer.to_string() %{total: total, items: paginated} = - Pagination.fetch_paginated(Object, %{"max_id" => id, "total" => true}) + Pagination.fetch_paginated(Object, %{max_id: id, total: true}) assert length(paginated) == 1 assert total == 5 @@ -50,7 +50,7 @@ test "paginates by max_id", %{notes: notes} do test "paginates by min_id & limit", %{notes: notes} do id = Enum.at(notes, 2).id |> Integer.to_string() - paginated = Pagination.fetch_paginated(Object, %{"min_id" => id, "limit" => 1}) + paginated = Pagination.fetch_paginated(Object, %{min_id: id, limit: 1}) assert length(paginated) == 1 end @@ -64,13 +64,13 @@ test "paginates by min_id & limit", %{notes: notes} do end test "paginates by limit" do - paginated = Pagination.fetch_paginated(Object, %{"limit" => 2}, :offset) + paginated = Pagination.fetch_paginated(Object, %{limit: 2}, :offset) assert length(paginated) == 2 end test "paginates by limit & offset" do - paginated = Pagination.fetch_paginated(Object, %{"limit" => 2, "offset" => 4}, :offset) + paginated = Pagination.fetch_paginated(Object, %{limit: 2, offset: 4}, :offset) assert length(paginated) == 1 end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 678288854..a8ba0658d 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -62,11 +62,11 @@ test "relay is unfollowed" do [undo_activity] = ActivityPub.fetch_activities([], %{ - "type" => "Undo", - "actor_id" => follower_id, - "limit" => 1, - "skip_preload" => true, - "invisible_actors" => true + type: "Undo", + actor_id: follower_id, + limit: 1, + skip_preload: true, + invisible_actors: true }) assert undo_activity.data["type"] == "Undo" diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index b55aa1cdb..9220d23fc 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -4,6 +4,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do alias Pleroma.Activity + alias Pleroma.MFA alias Pleroma.Object alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers @@ -278,6 +279,35 @@ test "no user to reset password" do end end + describe "running reset_mfa" do + test "disables MFA" do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true} + } + ) + + Mix.Tasks.Pleroma.User.run(["reset_mfa", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message == "Multi-Factor Authentication disabled for #{user.nickname}" + + assert %{enabled: false, totp: false} == + user.nickname + |> User.get_cached_by_nickname() + |> MFA.mfa_settings() + end + + test "no user to reset MFA" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + describe "running invite" do test "invite token is generated" do assert capture_io(fn -> diff --git a/test/upload_test.exs b/test/upload_test.exs index 060a940bb..2abf0edec 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -54,6 +54,7 @@ test "it returns file" do %{ "name" => "image.jpg", "type" => "Document", + "mediaType" => "image/jpeg", "url" => [ %{ "href" => "http://localhost:4001/media/post-process-file.jpg", diff --git a/test/user_test.exs b/test/user_test.exs index 6b344158d..98c79da4f 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1122,7 +1122,7 @@ test "hide a user's statuses from timelines and notifications" do assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - "user" => user2 + user: user2 }) {:ok, _user} = User.deactivate(user) @@ -1132,7 +1132,7 @@ test "hide a user's statuses from timelines and notifications" do assert [] == ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ - "user" => user2 + user: user2 }) end end @@ -1159,6 +1159,9 @@ test "it deactivates a user, all follow relationships and all activities", %{use follower = insert(:user) {:ok, follower} = User.follow(follower, user) + locked_user = insert(:user, name: "locked", locked: true) + {:ok, _} = User.follow(user, locked_user, :follow_pending) + object = insert(:note, user: user) activity = insert(:note_activity, user: user, note: object) @@ -1177,6 +1180,8 @@ test "it deactivates a user, all follow relationships and all activities", %{use refute User.following?(follower, user) assert %{deactivated: true} = User.get_by_id(user.id) + assert [] == User.get_follow_requests(locked_user) + user_activities = user.ap_id |> Activity.Queries.by_actor() diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 24edab41a..e490a5744 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -804,17 +804,63 @@ test "it requires authentication", %{conn: conn} do end describe "GET /users/:nickname/outbox" do + test "it paginates correctly", %{conn: conn} do + user = insert(:user) + conn = assign(conn, :user, user) + outbox_endpoint = user.ap_id <> "/outbox" + + _posts = + for i <- 0..25 do + {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) + activity + end + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get(outbox_endpoint <> "?page=true") + |> json_response(200) + + result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) + assert length(result["orderedItems"]) == 20 + assert length(result_ids) == 20 + assert result["next"] + assert String.starts_with?(result["next"], outbox_endpoint) + + result_next = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result["next"]) + |> json_response(200) + + result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) + assert length(result_next["orderedItems"]) == 6 + assert length(result_next_ids) == 6 + refute Enum.find(result_next_ids, fn x -> x in result_ids end) + refute Enum.find(result_ids, fn x -> x in result_next_ids end) + assert String.starts_with?(result["id"], outbox_endpoint) + + result_next_again = + conn + |> put_req_header("accept", "application/activity+json") + |> get(result_next["id"]) + |> json_response(200) + + assert result_next == result_next_again + end + test "it returns 200 even if there're no activities", %{conn: conn} do user = insert(:user) + outbox_endpoint = user.ap_id <> "/outbox" conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") - |> get("/users/#{user.nickname}/outbox") + |> get(outbox_endpoint) result = json_response(conn, 200) - assert user.ap_id <> "/outbox" == result["id"] + assert outbox_endpoint == result["id"] end test "it returns a note activity in a collection", %{conn: conn} do diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 3dcb62873..7693f6400 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -82,30 +82,28 @@ test "it restricts by the appropriate visibility" do {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"}) - activities = - ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id}) + activities = ActivityPub.fetch_activities([], %{visibility: "direct", actor_id: user.ap_id}) assert activities == [direct_activity] activities = - ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id}) + ActivityPub.fetch_activities([], %{visibility: "unlisted", actor_id: user.ap_id}) assert activities == [unlisted_activity] activities = - ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id}) + ActivityPub.fetch_activities([], %{visibility: "private", actor_id: user.ap_id}) assert activities == [private_activity] - activities = - ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id}) + activities = ActivityPub.fetch_activities([], %{visibility: "public", actor_id: user.ap_id}) assert activities == [public_activity] activities = ActivityPub.fetch_activities([], %{ - :visibility => ~w[private public], - "actor_id" => user.ap_id + visibility: ~w[private public], + actor_id: user.ap_id }) assert activities == [public_activity, private_activity] @@ -126,8 +124,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "direct", - "actor_id" => user.ap_id + exclude_visibilities: "direct", + actor_id: user.ap_id }) assert public_activity in activities @@ -137,8 +135,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "unlisted", - "actor_id" => user.ap_id + exclude_visibilities: "unlisted", + actor_id: user.ap_id }) assert public_activity in activities @@ -148,8 +146,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "private", - "actor_id" => user.ap_id + exclude_visibilities: "private", + actor_id: user.ap_id }) assert public_activity in activities @@ -159,8 +157,8 @@ test "it excludes by the appropriate visibility" do activities = ActivityPub.fetch_activities([], %{ - "exclude_visibilities" => "public", - "actor_id" => user.ap_id + exclude_visibilities: "public", + actor_id: user.ap_id }) refute public_activity in activities @@ -193,23 +191,22 @@ test "it fetches the appropriate tag-restricted posts" do {:ok, status_two} = CommonAPI.post(user, %{status: ". #essais"}) {:ok, status_three} = CommonAPI.post(user, %{status: ". #test #reject"}) - fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"}) + fetch_one = ActivityPub.fetch_activities([], %{type: "Create", tag: "test"}) - fetch_two = - ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]}) + fetch_two = ActivityPub.fetch_activities([], %{type: "Create", tag: ["test", "essais"]}) fetch_three = ActivityPub.fetch_activities([], %{ - "type" => "Create", - "tag" => ["test", "essais"], - "tag_reject" => ["reject"] + type: "Create", + tag: ["test", "essais"], + tag_reject: ["reject"] }) fetch_four = ActivityPub.fetch_activities([], %{ - "type" => "Create", - "tag" => ["test"], - "tag_all" => ["test", "reject"] + type: "Create", + tag: ["test"], + tag_all: ["test", "reject"] }) assert fetch_one == [status_one, status_three] @@ -375,7 +372,7 @@ test "can be fetched into a timeline" do _listen_activity_2 = insert(:listen) _listen_activity_3 = insert(:listen) - timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]}) + timeline = ActivityPub.fetch_activities([], %{type: ["Listen"]}) assert length(timeline) == 3 end @@ -507,7 +504,7 @@ test "retrieves activities that have a given context" do {:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]}) - activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) + activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user}) assert activities == [activity_two, activity] end end @@ -520,8 +517,7 @@ test "doesn't return blocked activities" do booster = insert(:user) {:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -529,8 +525,7 @@ test "doesn't return blocked activities" do {:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -541,16 +536,14 @@ test "doesn't return blocked activities" do %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) refute Enum.member?(activities, activity_three) refute Enum.member?(activities, boost_activity) assert Enum.member?(activities, activity_one) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: nil, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -573,7 +566,7 @@ test "doesn't return transitive interactions concerning blocked users" do {:ok, activity_four} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker}) assert Enum.member?(activities, activity_one) refute Enum.member?(activities, activity_two) @@ -581,7 +574,7 @@ test "doesn't return transitive interactions concerning blocked users" do refute Enum.member?(activities, activity_four) end - test "doesn't return announce activities concerning blocked users" do + test "doesn't return announce activities with blocked users in 'to'" do blocker = insert(:user) blockee = insert(:user) friend = insert(:user) @@ -595,7 +588,40 @@ test "doesn't return announce activities concerning blocked users" do {:ok, activity_three} = CommonAPI.repeat(activity_two.id, friend) activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker}) + ActivityPub.fetch_activities([], %{blocking_user: blocker}) + |> Enum.map(fn act -> act.id end) + + assert Enum.member?(activities, activity_one.id) + refute Enum.member?(activities, activity_two.id) + refute Enum.member?(activities, activity_three.id) + end + + test "doesn't return announce activities with blocked users in 'cc'" do + blocker = insert(:user) + blockee = insert(:user) + friend = insert(:user) + + {:ok, _user_relationship} = User.block(blocker, blockee) + + {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey!"}) + + {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) + + assert object = Pleroma.Object.normalize(activity_two) + + data = %{ + "actor" => friend.ap_id, + "object" => object.data["id"], + "context" => object.data["context"], + "type" => "Announce", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [blockee.ap_id] + } + + assert {:ok, activity_three} = ActivityPub.insert(data) + + activities = + ActivityPub.fetch_activities([], %{blocking_user: blocker}) |> Enum.map(fn act -> act.id end) assert Enum.member?(activities, activity_one.id) @@ -611,8 +637,7 @@ test "doesn't return activities from blocked domains" do user = insert(:user) {:ok, user} = User.block_domain(user, domain) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) refute activity in activities @@ -620,8 +645,7 @@ test "doesn't return activities from blocked domains" do ActivityPub.follow(user, followed_user) {:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true}) refute repeat_activity in activities end @@ -641,8 +665,7 @@ test "does return activities from followed users on blocked domains" do note = insert(:note, %{data: %{"actor" => domain_user.ap_id}}) activity = insert(:note_activity, %{note: note}) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) assert activity in activities @@ -653,8 +676,7 @@ test "does return activities from followed users on blocked domains" do bad_activity = insert(:note_activity, %{note: bad_note}) {:ok, repeat_activity} = CommonAPI.repeat(bad_activity.id, domain_user) - activities = - ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{blocking_user: blocker, skip_preload: true}) refute repeat_activity in activities end @@ -669,8 +691,7 @@ test "doesn't return muted activities" do activity_one_actor = User.get_by_ap_id(activity_one.data["actor"]) {:ok, _user_relationships} = User.mute(user, activity_one_actor) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -679,9 +700,9 @@ test "doesn't return muted activities" do # Calling with 'with_muted' will deliver muted activities, too. activities = ActivityPub.fetch_activities([], %{ - "muting_user" => user, - "with_muted" => true, - "skip_preload" => true + muting_user: user, + with_muted: true, + skip_preload: true }) assert Enum.member?(activities, activity_two) @@ -690,8 +711,7 @@ test "doesn't return muted activities" do {:ok, _user_mute} = User.unmute(user, activity_one_actor) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -703,15 +723,14 @@ test "doesn't return muted activities" do %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) activity_three = Activity.get_by_id(activity_three.id) - activities = - ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: user, skip_preload: true}) assert Enum.member?(activities, activity_two) refute Enum.member?(activities, activity_three) refute Enum.member?(activities, boost_activity) assert Enum.member?(activities, activity_one) - activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true}) + activities = ActivityPub.fetch_activities([], %{muting_user: nil, skip_preload: true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -727,7 +746,7 @@ test "doesn't return thread muted activities" do {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) - assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user}) + assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user}) end test "returns thread muted activities when with_muted is set" do @@ -739,7 +758,7 @@ test "returns thread muted activities when with_muted is set" do {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) assert [_activity_two, _activity_one] = - ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true}) + ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true}) end test "does include announces on request" do @@ -761,7 +780,7 @@ test "excludes reblogs on request" do {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) - [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"}) + [activity] = ActivityPub.fetch_user_activities(user, nil, %{exclude_reblogs: true}) assert activity == expected_activity end @@ -804,7 +823,7 @@ test "retrieves ids starting from a since_id" do expected_activities = ActivityBuilder.insert_list(10) since_id = List.last(activities).id - activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id}) + activities = ActivityPub.fetch_public_activities(%{since_id: since_id}) assert collect_ids(activities) == collect_ids(expected_activities) assert length(activities) == 10 @@ -819,7 +838,7 @@ test "retrieves ids up to max_id" do |> ActivityBuilder.insert_list() |> List.first() - activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id}) + activities = ActivityPub.fetch_public_activities(%{max_id: max_id}) assert length(activities) == 20 assert collect_ids(activities) == collect_ids(expected_activities) @@ -831,8 +850,7 @@ test "paginates via offset/limit" do later_activities = ActivityBuilder.insert_list(10) - activities = - ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset) + activities = ActivityPub.fetch_public_activities(%{page: "2", page_size: "20"}, :offset) assert length(activities) == 20 @@ -848,7 +866,7 @@ test "doesn't return reblogs for users for whom reblogs have been muted" do {:ok, activity} = CommonAPI.repeat(activity.id, booster) - activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + activities = ActivityPub.fetch_activities([], %{muting_user: user}) refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) end @@ -862,7 +880,7 @@ test "returns reblogs for users for whom reblogs have not been muted" do {:ok, activity} = CommonAPI.repeat(activity.id, booster) - activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + activities = ActivityPub.fetch_activities([], %{muting_user: user}) assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) end @@ -1066,7 +1084,7 @@ test "it filters broken threads" do assert length(activities) == 3 activities = - ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1}) + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{user: user1}) |> Enum.map(fn a -> a.id end) assert [public_activity.id, private_activity_1.id] == activities @@ -1115,7 +1133,7 @@ test "returned pinned statuses" do CommonAPI.pin(activity_three.id, user) user = refresh_record(user) - activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"}) + activities = ActivityPub.fetch_user_activities(user, nil, %{pinned: true}) assert 3 = length(activities) end @@ -1226,7 +1244,7 @@ test "fetch_activities/2 returns activities addressed to a list " do activity = Repo.preload(activity, :bookmark) activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} - assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity] + assert ActivityPub.fetch_activities([], %{user: user}) == [activity] end def data_uri do @@ -1400,7 +1418,7 @@ test "returns a favourite activities sorted by adds to favorite" do assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] - result = ActivityPub.fetch_favourites(user, %{"limit" => 2}) + result = ActivityPub.fetch_favourites(user, %{limit: 2}) assert Enum.map(result, & &1.id) == [a1.id, a5.id] end end @@ -1470,7 +1488,7 @@ test "doesn't retrieve replies activities with exclude_replies" do {:ok, _reply} = CommonAPI.post(user, %{status: "yeah", in_reply_to_status_id: activity.id}) - [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"}) + [result] = ActivityPub.fetch_public_activities(%{exclude_replies: true}) assert result.id == activity.id @@ -1483,11 +1501,11 @@ test "doesn't retrieve replies activities with exclude_replies" do test "public timeline", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1504,12 +1522,12 @@ test "public timeline with reply_visibility `following`", %{ } do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1531,12 +1549,12 @@ test "public timeline with reply_visibility `self`", %{ } do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1555,11 +1573,11 @@ test "home timeline", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1593,12 +1611,12 @@ test "home timeline with reply_visibility `following`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1632,12 +1650,12 @@ test "home timeline with reply_visibility `self`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1658,6 +1676,40 @@ test "home timeline with reply_visibility `self`", %{ assert Enum.all?(visible_ids, &(&1 in activities_ids)) end + + test "filtering out announces where the user is the actor of the announced message" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + User.follow(user, other_user) + + {:ok, post} = CommonAPI.post(user, %{status: "yo"}) + {:ok, other_post} = CommonAPI.post(third_user, %{status: "yo"}) + {:ok, _announce} = CommonAPI.repeat(post.id, other_user) + {:ok, _announce} = CommonAPI.repeat(post.id, third_user) + {:ok, announce} = CommonAPI.repeat(other_post.id, other_user) + + params = %{ + type: ["Announce"] + } + + results = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert length(results) == 3 + + params = %{ + type: ["Announce"], + announce_filtering_user: user + } + + [result] = + [user.ap_id | User.following(user)] + |> ActivityPub.fetch_activities(params) + + assert result.id == announce.id + end end describe "replies filtering with private messages" do @@ -1666,11 +1718,11 @@ test "home timeline with reply_visibility `self`", %{ test "public timeline", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1680,13 +1732,13 @@ test "public timeline", %{users: %{u1: user}} do test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1696,13 +1748,13 @@ test "public timeline with default reply_visibility `following`", %{users: %{u1: test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do activities_ids = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("local_only", false) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:local_only, false) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) + |> Map.put(:user, user) |> ActivityPub.fetch_public_activities() |> Enum.map(& &1.id) @@ -1712,10 +1764,10 @@ test "public timeline with default reply_visibility `self`", %{users: %{u1: user test "home timeline", %{users: %{u1: user}} do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1727,12 +1779,12 @@ test "home timeline", %{users: %{u1: user}} do test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "following") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "following") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -1751,12 +1803,12 @@ test "home timeline with default reply_visibility `self`", %{ } do params = %{} - |> Map.put("type", ["Create", "Announce"]) - |> Map.put("blocking_user", user) - |> Map.put("muting_user", user) - |> Map.put("user", user) - |> Map.put("reply_visibility", "self") - |> Map.put("reply_filtering_user", user) + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:blocking_user, user) + |> Map.put(:muting_user, user) + |> Map.put(:user, user) + |> Map.put(:reply_visibility, "self") + |> Map.put(:reply_filtering_user, user) activities_ids = ActivityPub.fetch_activities([user.ap_id | User.following(user)], params) @@ -2001,4 +2053,20 @@ test "it just returns the input if the user has no following/follower addresses" end) =~ "Follower/Following counter update for #{user.ap_id} failed" end end + + describe "global activity expiration" do + setup do: clear_config([:instance, :rewrite_policy]) + + test "creates an activity expiration for local Create activities" do + Pleroma.Config.put( + [:instance, :rewrite_policy], + Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + ) + + {:ok, %{id: id_create}} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) + {:ok, _follow} = ActivityBuilder.insert(%{"type" => "Follow", "context" => "3hu"}) + + assert [%{activity_id: ^id_create}] = Pleroma.ActivityExpiration |> Repo.all() + end + end end diff --git a/test/web/activity_pub/mrf/activity_expiration_policy_test.exs b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs new file mode 100644 index 000000000..8babf49e7 --- /dev/null +++ b/test/web/activity_pub/mrf/activity_expiration_policy_test.exs @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do + use ExUnit.Case, async: true + alias Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + + @id Pleroma.Web.Endpoint.url() <> "/activities/cofe" + + test "adds `expires_at` property" do + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + end + + test "keeps existing `expires_at` if it less than the config setting" do + expires_at = NaiveDateTime.utc_now() |> Timex.shift(days: 1) + + assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "expires_at" => expires_at, + "object" => %{"type" => "Note"} + }) + end + + test "overwrites existing `expires_at` if it greater than the config setting" do + too_distant_future = NaiveDateTime.utc_now() |> Timex.shift(years: 2) + + assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} = + ActivityExpirationPolicy.filter(%{ + "id" => @id, + "type" => "Create", + "expires_at" => too_distant_future, + "object" => %{"type" => "Note"} + }) + + assert Timex.diff(expires_at, NaiveDateTime.utc_now(), :days) == 364 + end + + test "ignores remote activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Create", + "object" => %{"type" => "Note"} + }) + + refute Map.has_key?(activity, "expires_at") + end + + test "ignores non-Create/Note activities" do + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Follow" + }) + + refute Map.has_key?(activity, "expires_at") + + assert {:ok, activity} = + ActivityExpirationPolicy.filter(%{ + "id" => "https://example.com/123", + "type" => "Create", + "object" => %{"type" => "Cofe"} + }) + + refute Map.has_key?(activity, "expires_at") + end +end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 7953eecf2..31224abe0 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -2,14 +2,264 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do use Pleroma.DataCase alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI import Pleroma.Factory + describe "attachments" do + test "works with honkerific attachments" do + attachment = %{ + "mediaType" => "", + "name" => "", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + + assert {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "application/octet-stream" + end + + test "it turns mastodon attachments into our attachments" do + attachment = %{ + "url" => + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type" => "Document", + "name" => nil, + "mediaType" => "image/jpeg" + } + + {:ok, attachment} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + + assert [ + %{ + href: + "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + type: "Link", + mediaType: "image/jpeg" + } + ] = attachment.url + + assert attachment.mediaType == "image/jpeg" + end + + test "it handles our own uploads" do + user = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, attachment} = + attachment.data + |> AttachmentValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) + + assert attachment.mediaType == "image/jpeg" + end + end + + describe "chat message create activities" do + test "it is invalid if the object already exists" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(activity, false) + + {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:object, {"The object to create already exists", []}} in cng.errors + end + + test "it is invalid if the object data has a different `to` or `actor` field" do + user = insert(:user) + recipient = insert(:user) + {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey") + + {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id]) + + {:error, cng} = ObjectValidator.validate(create_data, []) + + assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors + assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors + end + end + + describe "chat messages" do + setup do + clear_config([:instance, :remote_limit]) + user = insert(:user) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:") + + %{user: user, recipient: recipient, valid_chat_message: valid_chat_message} + end + + test "let's through some basic html", %{user: user, recipient: recipient} do + {:ok, valid_chat_message, _} = + Builder.chat_message( + user, + recipient.ap_id, + "hey example " + ) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["content"] == + "hey example alert('uguu')" + end + + test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert Map.put(valid_chat_message, "attachment", nil) == object + end + + test "validates for a basic object with an attachment", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment in an array", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", [attachment.data]) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "validates for a basic object with an attachment but without content", %{ + valid_chat_message: valid_chat_message, + user: user + } do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", attachment.data) + |> Map.delete("content") + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object["attachment"] + end + + test "does not validate if the message has no content", %{ + valid_chat_message: valid_chat_message + } do + contentless = + valid_chat_message + |> Map.delete("content") + + refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, [])) + end + + test "does not validate if the message is longer than the remote_limit", %{ + valid_chat_message: valid_chat_message + } do + Pleroma.Config.put([:instance, :remote_limit], 2) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the recipient is blocking the actor", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + Pleroma.User.block(recipient, user) + refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, [])) + end + + test "does not validate if the actor or the recipient is not in our system", %{ + valid_chat_message: valid_chat_message + } do + chat_message = + valid_chat_message + |> Map.put("actor", "https://raymoo.com/raymoo") + + {:error, _} = ObjectValidator.validate(chat_message, []) + + chat_message = + valid_chat_message + |> Map.put("to", ["https://raymoo.com/raymoo"]) + + {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate for a message with multiple recipients", %{ + valid_chat_message: valid_chat_message, + user: user, + recipient: recipient + } do + chat_message = + valid_chat_message + |> Map.put("to", [user.ap_id, recipient.ap_id]) + + assert {:error, _} = ObjectValidator.validate(chat_message, []) + end + + test "does not validate if it doesn't concern local users" do + user = insert(:user, local: false) + recipient = insert(:user, local: false) + + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey") + assert {:error, _} = ObjectValidator.validate(valid_chat_message, []) + end + end + describe "EmojiReacts" do setup do user = insert(:user) diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs index 834213182..c8911948e 100644 --- a/test/web/activity_pub/object_validators/types/object_id_test.exs +++ b/test/web/activity_pub/object_validators/types/object_id_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID use Pleroma.DataCase diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs new file mode 100644 index 000000000..d4a574554 --- /dev/null +++ b/test/web/activity_pub/object_validators/types/safe_text_test.exs @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText + + test "it lets normal text go through" do + text = "hey how are you" + assert {:ok, text} == SafeText.cast(text) + end + + test "it removes html tags from text" do + text = "hey look xss " + assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text) + end + + test "it keeps basic html tags" do + text = "hey look xss " + + assert {:ok, "hey look xss alert('foo')"} == + SafeText.cast(text) + end + + test "errors for non-text" do + assert :error == SafeText.cast(1) + end +end diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs index 26557720b..8deb64501 100644 --- a/test/web/activity_pub/pipeline_test.exs +++ b/test/web/activity_pub/pipeline_test.exs @@ -33,7 +33,10 @@ test "it goes through validation, filtering, persisting, side effects and federa { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [ + handle: fn o, m -> {:ok, o, m} end, + handle_after_transaction: fn m -> m end + ] }, { Pleroma.Web.Federator, @@ -71,7 +74,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] }, { Pleroma.Web.Federator, @@ -110,7 +113,7 @@ test "it goes through validation, filtering, persisting, side effects without fe { Pleroma.Web.ActivityPub.SideEffects, [], - [handle: fn o, m -> {:ok, o, m} end] + [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end] }, { Pleroma.Web.Federator, diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index a80104ea7..6bbbaae87 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -20,6 +22,48 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do import Pleroma.Factory import Mock + describe "handle_after_transaction" do + test "it streams out notifications and streams" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [notification] = meta[:notifications] + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + SideEffects.handle_after_transaction(meta) + + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + assert called(Pleroma.Web.Push.send(notification)) + end + end + end + describe "delete objects" do setup do user = insert(:user) @@ -290,6 +334,147 @@ test "creates a notification", %{like: like, poster: poster} do end end + describe "creation of ChatMessages" do + test "notifies the recipient" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id) + end + + test "it streams the created ChatMessage" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + assert [_, _] = meta[:streamables] + end + + test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> nil end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, _create_activity, meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # The notification gets created + assert [notification] = meta[:notifications] + assert notification.activity_id == create_activity.id + + # But it is not sent out + refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + refute called(Pleroma.Web.Push.send(notification)) + + # Same for the user chat stream + assert [{topics, _}, _] = meta[:streamables] + assert topics == ["user", "user:pleroma_chat"] + refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + chat = Chat.get(author.id, recipient.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == false + + chat = Chat.get(recipient.id, author.ap_id) + + [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all() + + assert cm_ref.object.data["content"] == "hey" + assert cm_ref.unread == true + end + end + + test "it creates a Chat for the local users and bumps the unread count" do + author = insert(:user, local: false) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # An object is created + assert Object.get_by_ap_id(chat_message_data["id"]) + + # The remote user won't get a chat + chat = Chat.get(author.id, recipient.ap_id) + refute chat + + # The local user will get a chat + chat = Chat.get(recipient.id, author.ap_id) + assert chat + + author = insert(:user, local: true) + recipient = insert(:user, local: true) + + {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey") + + {:ok, create_activity_data, _meta} = + Builder.create(author, chat_message_data["id"], [recipient.ap_id]) + + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, local: false, object_data: chat_message_data) + + # Both users are local and get the chat + chat = Chat.get(author.id, recipient.ap_id) + assert chat + + chat = Chat.get(recipient.id, author.ap_id) + assert chat + end + end + describe "announce objects" do setup do poster = insert(:user) diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs new file mode 100644 index 000000000..d6736dc3e --- /dev/null +++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs @@ -0,0 +1,153 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + describe "handle_incoming" do + test "handles chonks with attachment" do + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "https://honk.tedunangst.com/u/tedu", + "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T", + "object" => %{ + "attachment" => [ + %{ + "mediaType" => "image/jpeg", + "name" => "298p3RG7j27tfsZ9RQ.jpg", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document", + "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg" + } + ], + "attributedTo" => "https://honk.tedunangst.com/u/tedu", + "content" => "", + "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b", + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "ChatMessage" + }, + "published" => "2020-05-18T01:13:03Z", + "to" => [ + "https://dontbulling.me/users/lain" + ], + "type" => "Create" + } + + _user = insert(:user, ap_id: data["actor"]) + _user = insert(:user, ap_id: hd(data["to"])) + + assert {:ok, _activity} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't contain content" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.delete("content") + + data = + data + |> Map.put("object", object) + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: true, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages that don't concern local users" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + _author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + _recipient = + insert(:user, + ap_id: List.first(data["to"]), + local: false, + last_refreshed_at: DateTime.utc_now() + ) + + {:error, _} = Transmogrifier.handle_incoming(data) + end + + test "it rejects messages where the `to` field of activity and object don't match" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = insert(:user, ap_id: data["actor"]) + _recipient = insert(:user, ap_id: List.first(data["to"])) + + data = + data + |> Map.put("to", author.ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + refute Object.get_by_ap_id(data["object"]["id"]) + end + + test "it fetches the actor if they aren't in our system" do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + |> Map.put("actor", "http://mastodon.example.org/users/admin") + |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin") + + _recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data) + end + + test "it inserts it and creates a chat" do + data = + File.read!("test/fixtures/create-chat-message.json") + |> Poison.decode!() + + author = + insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now()) + + recipient = insert(:user, ap_id: List.first(data["to"]), local: true) + + {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data) + assert activity.local == false + + assert activity.actor == author.ap_id + assert activity.recipients == [recipient.ap_id, author.ap_id] + + %Object{} = object = Object.get_by_ap_id(activity.data["object"]) + + assert object + assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')" + assert match?(%{"firefox" => _}, object.data["emoji"]) + + refute Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + end + end +end diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 967389fae..06c39eed6 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Notification alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier @@ -12,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do import Pleroma.Factory import Ecto.Query + import Mock setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -57,9 +59,12 @@ test "it works for incoming follow requests" do activity = Repo.get(Activity, activity.id) assert activity.data["state"] == "accept" assert User.following?(User.get_cached_by_ap_id(data["actor"]), user) + + [notification] = Notification.for_user(user) + assert notification.type == "follow" end - test "with locked accounts, it does not create a follow or an accept" do + test "with locked accounts, it does create a Follow, but not an Accept" do user = insert(:user, locked: true) data = @@ -81,6 +86,9 @@ test "with locked accounts, it does not create a follow or an accept" do |> Repo.all() assert Enum.empty?(accepts) + + [notification] = Notification.for_user(user) + assert notification.type == "follow_request" end test "it works for follow requests when you are already followed, creating a new accept activity" do @@ -144,6 +152,23 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl assert activity.data["state"] == "reject" end + test "it rejects incoming follow requests if the following errors for some reason" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do + {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data) + + %Activity{} = activity = Activity.get_by_ap_id(id) + + assert activity.data["state"] == "reject" + end + end + test "it works for incoming follow requests from hubzilla" do user = insert(:user) diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 20b0f223c..bec15a996 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -158,35 +158,4 @@ test "sets correct totalItems when follows are hidden but the follow counter is assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) end end - - test "activity collection page aginates correctly" do - user = insert(:user) - - posts = - for i <- 0..25 do - {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) - activity - end - - # outbox sorts chronologically, newest first, with ten per page - posts = Enum.reverse(posts) - - %{"next" => next_url} = - UserView.render("activity_collection_page.json", %{ - iri: "#{user.ap_id}/outbox", - activities: Enum.take(posts, 10) - }) - - next_id = Enum.at(posts, 9).id - assert next_url =~ next_id - - %{"next" => next_url} = - UserView.render("activity_collection_page.json", %{ - iri: "#{user.ap_id}/outbox", - activities: Enum.take(Enum.drop(posts, 10), 10) - }) - - next_id = Enum.at(posts, 19).id - assert next_url =~ next_id - end end diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs index a1bff5688..e3d3ccb8d 100644 --- a/test/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/web/admin_api/controllers/admin_api_controller_test.exs @@ -12,7 +12,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.Activity alias Pleroma.Config - alias Pleroma.ConfigDB alias Pleroma.HTML alias Pleroma.MFA alias Pleroma.ModerationLog @@ -338,7 +337,8 @@ test "Show", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } assert expected == json_response(conn, 200) @@ -615,7 +615,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => user.deactivated, @@ -626,7 +627,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do "tags" => ["foo", "bar"], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -698,7 +700,8 @@ test "regular search", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -723,7 +726,8 @@ test "search by domain", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -748,7 +752,8 @@ test "search by full nickname", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -773,7 +778,8 @@ test "search by display name", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -798,7 +804,8 @@ test "search by email", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -823,7 +830,8 @@ test "regular search with page size", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -843,7 +851,8 @@ test "regular search with page size", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(user2) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user2.ap_id } ] } @@ -875,7 +884,8 @@ test "only local users" do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -900,7 +910,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id }, %{ "deactivated" => admin.deactivated, @@ -911,7 +922,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => false, @@ -922,7 +934,8 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do "tags" => [], "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => old_admin.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -952,7 +965,8 @@ test "load only admins", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id }, %{ "deactivated" => false, @@ -963,7 +977,8 @@ test "load only admins", %{conn: conn, admin: admin} do "tags" => [], "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => second_admin.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -995,7 +1010,8 @@ test "load only moderators", %{conn: conn} do "tags" => [], "avatar" => User.avatar_url(moderator) |> MediaProxy.url(), "display_name" => HTML.strip_tags(moderator.name || moderator.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => moderator.ap_id } ] } @@ -1020,7 +1036,8 @@ test "load users with tags list", %{conn: conn} do "tags" => ["first"], "avatar" => User.avatar_url(user1) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user1.name || user1.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user1.ap_id }, %{ "deactivated" => false, @@ -1031,7 +1048,8 @@ test "load users with tags list", %{conn: conn} do "tags" => ["second"], "avatar" => User.avatar_url(user2) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user2.name || user2.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user2.ap_id } ] |> Enum.sort_by(& &1["nickname"]) @@ -1070,7 +1088,8 @@ test "it works with multiple filters" do "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } ] } @@ -1094,7 +1113,8 @@ test "it omits relay user", %{admin: admin, conn: conn} do "tags" => [], "avatar" => User.avatar_url(admin) |> MediaProxy.url(), "display_name" => HTML.strip_tags(admin.name || admin.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => admin.ap_id } ] } @@ -1156,7 +1176,8 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admi "tags" => [], "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname), - "confirmation_pending" => false + "confirmation_pending" => false, + "url" => user.ap_id } log_entry = Repo.one(ModerationLog) @@ -1197,1175 +1218,6 @@ test "returns 404 if user not found", %{conn: conn} do end end - describe "GET /api/pleroma/admin/config" do - setup do: clear_config(:configurable_from_database, true) - - test "when configuration from database is off", %{conn: conn} do - Config.put(:configurable_from_database, false) - conn = get(conn, "/api/pleroma/admin/config") - - assert json_response(conn, 400) == - %{ - "error" => "To use this endpoint you need to enable configuration from database." - } - end - - test "with settings only in db", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - conn = get(conn, "/api/pleroma/admin/config", %{"only_db" => true}) - - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => key1, - "value" => _ - }, - %{ - "group" => ":pleroma", - "key" => key2, - "value" => _ - } - ] - } = json_response(conn, 200) - - assert key1 == config1.key - assert key2 == config2.key - end - - test "db is added to settings that are in db", %{conn: conn} do - _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name")) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - [instance_config] = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key == ":instance" - end) - - assert instance_config["db"] == [":name"] - end - - test "merged default setting with db settings", %{conn: conn} do - config1 = insert(:config) - config2 = insert(:config) - - config3 = - insert(:config, - value: ConfigDB.to_binary(k1: :v1, k2: :v2) - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert length(configs) > 3 - - received_configs = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key, config3.key] - end) - - assert length(received_configs) == 3 - - db_keys = - config3.value - |> ConfigDB.from_binary() - |> Keyword.keys() - |> ConfigDB.convert() - - Enum.each(received_configs, fn %{"value" => value, "db" => db} -> - assert db in [[config1.key], [config2.key], db_keys] - - assert value in [ - ConfigDB.from_binary_with_convert(config1.value), - ConfigDB.from_binary_with_convert(config2.value), - ConfigDB.from_binary_with_convert(config3.value) - ] - end) - end - - test "subkeys with full update right merge", %{conn: conn} do - config1 = - insert(:config, - key: ":emoji", - value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) - ) - - config2 = - insert(:config, - key: ":assets", - value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) - ) - - %{"configs" => configs} = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - vals = - Enum.filter(configs, fn %{"group" => group, "key" => key} -> - group == ":pleroma" and key in [config1.key, config2.key] - end) - - emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) - assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) - - emoji_val = ConfigDB.transform_with_out_binary(emoji["value"]) - assets_val = ConfigDB.transform_with_out_binary(assets["value"]) - - assert emoji_val[:groups] == [a: 1, b: 2] - assert assets_val[:mascots] == [a: 1, b: 2] - end - end - - test "POST /api/pleroma/admin/config error", %{conn: conn} do - conn = post(conn, "/api/pleroma/admin/config", %{"configs" => []}) - - assert json_response(conn, 400) == - %{"error" => "To use this endpoint you need to enable configuration from database."} - end - - describe "POST /api/pleroma/admin/config" do - setup do - http = Application.get_env(:pleroma, :http) - - on_exit(fn -> - Application.delete_env(:pleroma, :key1) - Application.delete_env(:pleroma, :key2) - Application.delete_env(:pleroma, :key3) - Application.delete_env(:pleroma, :key4) - Application.delete_env(:pleroma, :keyaa1) - Application.delete_env(:pleroma, :keyaa2) - Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) - Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) - Application.put_env(:pleroma, :http, http) - Application.put_env(:tesla, :adapter, Tesla.Mock) - Restarter.Pleroma.refresh() - end) - end - - setup do: clear_config(:configurable_from_database, true) - - @tag capture_log: true - test "create new config setting in db", %{conn: conn} do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{ - group: ":ueberauth", - key: "Ueberauth", - value: [%{"tuple" => [":consumer_secret", "aaaa"]}] - }, - %{ - group: ":pleroma", - key: ":key2", - value: %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - } - }, - %{ - group: ":pleroma", - key: ":key3", - value: [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - }, - %{ - group: ":pleroma", - key: ":key4", - value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} - }, - %{ - group: ":idna", - key: ":key5", - value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => "value1", - "db" => [":key1"] - }, - %{ - "group" => ":ueberauth", - "key" => "Ueberauth", - "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], - "db" => [":consumer_secret"] - }, - %{ - "group" => ":pleroma", - "key" => ":key2", - "value" => %{ - ":nested_1" => "nested_value1", - ":nested_2" => [ - %{":nested_22" => "nested_value222"}, - %{":nested_33" => %{":nested_44" => "nested_444"}} - ] - }, - "db" => [":key2"] - }, - %{ - "group" => ":pleroma", - "key" => ":key3", - "value" => [ - %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, - %{"nested_4" => true} - ], - "db" => [":key3"] - }, - %{ - "group" => ":pleroma", - "key" => ":key4", - "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, - "db" => [":key4"] - }, - %{ - "group" => ":idna", - "key" => ":key5", - "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, - "db" => [":key5"] - } - ] - } - - assert Application.get_env(:pleroma, :key1) == "value1" - - assert Application.get_env(:pleroma, :key2) == %{ - nested_1: "nested_value1", - nested_2: [ - %{nested_22: "nested_value222"}, - %{nested_33: %{nested_44: "nested_444"}} - ] - } - - assert Application.get_env(:pleroma, :key3) == [ - %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, - %{"nested_4" => true} - ] - - assert Application.get_env(:pleroma, :key4) == %{ - "endpoint" => "https://example.com", - nested_5: :upload - } - - assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} - end - - test "save configs setting without explicit key", %{conn: conn} do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) - webhook_url = Application.get_env(:quack, :webhook_url) - - on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - Application.put_env(:quack, :webhook_url, webhook_url) - end) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":quack", - key: ":level", - value: ":info" - }, - %{ - group: ":quack", - key: ":meta", - value: [":none"] - }, - %{ - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY" - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":quack", - "key" => ":level", - "value" => ":info", - "db" => [":level"] - }, - %{ - "group" => ":quack", - "key" => ":meta", - "value" => [":none"], - "db" => [":meta"] - }, - %{ - "group" => ":quack", - "key" => ":webhook_url", - "value" => "https://hooks.slack.com/services/KEY", - "db" => [":webhook_url"] - } - ] - } - - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" - end - - test "saving config with partial update", %{conn: conn} do - config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key2", 2]}, - %{"tuple" => [":key3", 3]} - ], - "db" => [":key1", ":key2", ":key3"] - } - ] - } - end - - test "saving config which need pleroma reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert post( - conn, - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] == false - end - - test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do - chat = Config.get(:chat) - on_exit(fn -> Config.put(:chat, chat) end) - - assert post( - conn, - "/api/pleroma/admin/config", - %{ - configs: [ - %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} - ] - } - ) - |> json_response(200) == %{ - "configs" => [ - %{ - "db" => [":enabled"], - "group" => ":pleroma", - "key" => ":chat", - "value" => [%{"tuple" => [":enabled", true]}] - } - ], - "need_reboot" => true - } - - assert post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} - ] - }) - |> json_response(200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key3", 3]} - ], - "db" => [":key3"] - } - ], - "need_reboot" => true - } - - capture_log(fn -> - assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{} - end) =~ "pleroma restarted" - - configs = - conn - |> get("/api/pleroma/admin/config") - |> json_response(200) - - assert configs["need_reboot"] == false - end - - test "saving config with nested merge", %{conn: conn} do - config = - insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - value: [ - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key1", 1]}, - %{"tuple" => [":key3", 3]}, - %{ - "tuple" => [ - ":key2", - [ - %{"tuple" => [":k1", 1]}, - %{"tuple" => [":k2", 1]}, - %{"tuple" => [":k3", 3]} - ] - ] - } - ], - "db" => [":key1", ":key3", ":key2"] - } - ] - } - end - - test "saving special atoms", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{ - "tuple" => [ - ":ssl_options", - [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] - ] - } - ], - "db" => [":ssl_options"] - } - ] - } - - assert Application.get_env(:pleroma, :key1) == [ - ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] - ] - end - - test "saving full setting if value is in full_key_update list", %{conn: conn} do - backends = Application.get_env(:logger, :backends) - on_exit(fn -> Application.put_env(:logger, :backends, backends) end) - - config = - insert(:config, - group: ":logger", - key: ":backends", - value: :erlang.term_to_binary([]) - ) - - Pleroma.Config.TransferTask.load_and_update_env([], false) - - assert Application.get_env(:logger, :backends) == [] - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - value: [":console"] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":logger", - "key" => ":backends", - "value" => [ - ":console" - ], - "db" => [":backends"] - } - ] - } - - assert Application.get_env(:logger, :backends) == [ - :console - ] - end - - test "saving full setting if value is not keyword", %{conn: conn} do - config = - insert(:config, - group: ":tesla", - key: ":adapter", - value: :erlang.term_to_binary(Tesla.Adapter.Hackey) - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":tesla", - "key" => ":adapter", - "value" => "Tesla.Adapter.Httpc", - "db" => [":adapter"] - } - ] - } - end - - test "update config setting & delete with fallback to default value", %{ - conn: conn, - admin: admin, - token: token - } do - ueberauth = Application.get_env(:ueberauth, Ueberauth) - config1 = insert(:config, key: ":keyaa1") - config2 = insert(:config, key: ":keyaa2") - - config3 = - insert(:config, - group: ":ueberauth", - key: "Ueberauth" - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: config1.group, key: config1.key, value: "another_value"}, - %{group: config2.group, key: config2.key, value: "another_value"} - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => config1.key, - "value" => "another_value", - "db" => [":keyaa1"] - }, - %{ - "group" => ":pleroma", - "key" => config2.key, - "value" => "another_value", - "db" => [":keyaa2"] - } - ] - } - - assert Application.get_env(:pleroma, :keyaa1) == "another_value" - assert Application.get_env(:pleroma, :keyaa2) == "another_value" - assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value) - - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - |> post("/api/pleroma/admin/config", %{ - configs: [ - %{group: config2.group, key: config2.key, delete: true}, - %{ - group: ":ueberauth", - key: "Ueberauth", - delete: true - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [] - } - - assert Application.get_env(:ueberauth, Ueberauth) == ueberauth - refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) - end - - test "common config example", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ] - } - ] - }) - - assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Captcha.NotReal", - "value" => [ - %{"tuple" => [":enabled", false]}, - %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, - %{"tuple" => [":seconds_valid", 60]}, - %{"tuple" => [":path", ""]}, - %{"tuple" => [":key1", nil]}, - %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, - %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, - %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, - %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, - %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, - %{"tuple" => [":name", "Pleroma"]} - ], - "db" => [ - ":enabled", - ":method", - ":seconds_valid", - ":path", - ":key1", - ":partial_chain", - ":regex1", - ":regex2", - ":regex3", - ":regex4", - ":name" - ] - } - ] - } - end - - test "tuples with more than two values", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => "Pleroma.Web.Endpoint.NotReal", - "value" => [ - %{ - "tuple" => [ - ":http", - [ - %{ - "tuple" => [ - ":key2", - [ - %{ - "tuple" => [ - ":_", - [ - %{ - "tuple" => [ - "/api/v1/streaming", - "Pleroma.Web.MastodonAPI.WebsocketHandler", - [] - ] - }, - %{ - "tuple" => [ - "/websocket", - "Phoenix.Endpoint.CowboyWebSocket", - %{ - "tuple" => [ - "Phoenix.Transports.WebSocket", - %{ - "tuple" => [ - "Pleroma.Web.Endpoint", - "Pleroma.Web.UserSocket", - [] - ] - } - ] - } - ] - }, - %{ - "tuple" => [ - ":_", - "Phoenix.Endpoint.Cowboy2Handler", - %{"tuple" => ["Pleroma.Web.Endpoint", []]} - ] - } - ] - ] - } - ] - ] - } - ] - ] - } - ], - "db" => [":http"] - } - ] - } - end - - test "settings with nesting map", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000, - "nested" => %{ - ":max_options" => 20, - ":max_option_chars" => 200, - ":min_expiration" => 0, - ":max_expiration" => 31_536_000 - } - } - ] - } - ] - } - ] - }) - - assert json_response(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => [ - %{"tuple" => [":key2", "some_val"]}, - %{ - "tuple" => [ - ":key3", - %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0, - "nested" => %{ - ":max_expiration" => 31_536_000, - ":max_option_chars" => 200, - ":max_options" => 20, - ":min_expiration" => 0 - } - } - ] - } - ], - "db" => [":key2", ":key3"] - } - ] - } - end - - test "value as map", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"} - } - ] - }) - - assert json_response(conn, 200) == - %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":key1", - "value" => %{"key" => "some_val"}, - "db" => [":key1"] - } - ] - } - end - - test "queues key as atom", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ] - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":oban", - "key" => ":queues", - "value" => [ - %{"tuple" => [":federator_incoming", 50]}, - %{"tuple" => [":federator_outgoing", 50]}, - %{"tuple" => [":web_push", 50]}, - %{"tuple" => [":mailer", 10]}, - %{"tuple" => [":transmogrifier", 20]}, - %{"tuple" => [":scheduled_activities", 10]}, - %{"tuple" => [":background", 5]} - ], - "db" => [ - ":federator_incoming", - ":federator_outgoing", - ":web_push", - ":mailer", - ":transmogrifier", - ":scheduled_activities", - ":background" - ] - } - ] - } - end - - test "delete part of settings by atom subkeys", %{conn: conn} do - config = - insert(:config, - key: ":keyaa1", - value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") - ) - - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: config.group, - key: config.key, - subkeys: [":subkey1", ":subkey3"], - delete: true - } - ] - }) - - assert json_response(conn, 200) == %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":keyaa1", - "value" => [%{"tuple" => [":subkey2", "val2"]}], - "db" => [":subkey2"] - } - ] - } - end - - test "proxy tuple localhost", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple domain", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value - assert ":proxy_url" in db - end - - test "proxy tuple ip", %{conn: conn} do - conn = - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{ - group: ":pleroma", - key: ":http", - value: [ - %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} - ] - } - ] - }) - - assert %{ - "configs" => [ - %{ - "group" => ":pleroma", - "key" => ":http", - "value" => value, - "db" => db - } - ] - } = json_response(conn, 200) - - assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value - assert ":proxy_url" in db - end - - @tag capture_log: true - test "doesn't set keys not in the whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :key1}, - {:pleroma, :key2}, - {:pleroma, Pleroma.Captcha.NotReal}, - {:not_real} - ]) - - post(conn, "/api/pleroma/admin/config", %{ - configs: [ - %{group: ":pleroma", key: ":key1", value: "value1"}, - %{group: ":pleroma", key: ":key2", value: "value2"}, - %{group: ":pleroma", key: ":key3", value: "value3"}, - %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, - %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, - %{group: ":not_real", key: ":anything", value: "value6"} - ] - }) - - assert Application.get_env(:pleroma, :key1) == "value1" - assert Application.get_env(:pleroma, :key2) == "value2" - assert Application.get_env(:pleroma, :key3) == nil - assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil - assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" - assert Application.get_env(:not_real, :anything) == "value6" - end - end - describe "GET /api/pleroma/admin/restart" do setup do: clear_config(:configurable_from_database, true) @@ -2774,57 +1626,6 @@ test "sets password_reset_pending to true", %{conn: conn} do end end - describe "relays" do - test "POST /relay", %{conn: conn, admin: admin} do - conn = - post(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - end - - test "GET /relay", %{conn: conn} do - relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() - - ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] - |> Enum.each(fn ap_id -> - {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) - User.follow(relay_user, user) - end) - - conn = get(conn, "/api/pleroma/admin/relay") - - assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == [] - end - - test "DELETE /relay", %{conn: conn, admin: admin} do - post(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - conn = - delete(conn, "/api/pleroma/admin/relay", %{ - relay_url: "http://mastodon.example.org/users/admin" - }) - - assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" - - [log_entry_one, log_entry_two] = Repo.all(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry_one) == - "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" - - assert ModerationLog.get_log_entry_message(log_entry_two) == - "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" - end - end - describe "instances" do test "GET /instances/:instance/statuses", %{conn: conn} do user = insert(:user, local: false, nickname: "archaeme@archae.me") @@ -2914,56 +1715,6 @@ test "it resend emails for two users", %{conn: conn, admin: admin} do end end - describe "GET /api/pleroma/admin/config/descriptions" do - test "structure", %{conn: conn} do - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - assert [child | _others] = json_response(conn, 200) - - assert child["children"] - assert child["key"] - assert String.starts_with?(child["group"], ":") - assert child["description"] - end - - test "filters by database configuration whitelist", %{conn: conn} do - clear_config(:database_config_whitelist, [ - {:pleroma, :instance}, - {:pleroma, :activitypub}, - {:pleroma, Pleroma.Upload}, - {:esshd} - ]) - - admin = insert(:user, is_admin: true) - - conn = - assign(conn, :user, admin) - |> get("/api/pleroma/admin/config/descriptions") - - children = json_response(conn, 200) - - assert length(children) == 4 - - assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 - - instance = Enum.find(children, fn c -> c["key"] == ":instance" end) - assert instance["children"] - - activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) - assert activitypub["children"] - - web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) - assert web_endpoint["children"] - - esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) - assert esshd["children"] - end - end - describe "/api/pleroma/admin/stats" do test "status visibility count", %{conn: conn} do admin = insert(:user, is_admin: true) diff --git a/test/web/admin_api/controllers/config_controller_test.exs b/test/web/admin_api/controllers/config_controller_test.exs new file mode 100644 index 000000000..780de8d18 --- /dev/null +++ b/test/web/admin_api/controllers/config_controller_test.exs @@ -0,0 +1,1290 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do + use Pleroma.Web.ConnCase, async: true + + import ExUnit.CaptureLog + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ConfigDB + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/config" do + setup do: clear_config(:configurable_from_database, true) + + test "when configuration from database is off", %{conn: conn} do + Config.put(:configurable_from_database, false) + conn = get(conn, "/api/pleroma/admin/config") + + assert json_response_and_validate_schema(conn, 400) == + %{ + "error" => "To use this endpoint you need to enable configuration from database." + } + end + + test "with settings only in db", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + conn = get(conn, "/api/pleroma/admin/config?only_db=true") + + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => key1, + "value" => _ + }, + %{ + "group" => ":pleroma", + "key" => key2, + "value" => _ + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert key1 == config1.key + assert key2 == config2.key + end + + test "db is added to settings that are in db", %{conn: conn} do + _config = insert(:config, key: ":instance", value: ConfigDB.to_binary(name: "Some name")) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + [instance_config] = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key == ":instance" + end) + + assert instance_config["db"] == [":name"] + end + + test "merged default setting with db settings", %{conn: conn} do + config1 = insert(:config) + config2 = insert(:config) + + config3 = + insert(:config, + value: ConfigDB.to_binary(k1: :v1, k2: :v2) + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert length(configs) > 3 + + received_configs = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [config1.key, config2.key, config3.key] + end) + + assert length(received_configs) == 3 + + db_keys = + config3.value + |> ConfigDB.from_binary() + |> Keyword.keys() + |> ConfigDB.convert() + + Enum.each(received_configs, fn %{"value" => value, "db" => db} -> + assert db in [[config1.key], [config2.key], db_keys] + + assert value in [ + ConfigDB.from_binary_with_convert(config1.value), + ConfigDB.from_binary_with_convert(config2.value), + ConfigDB.from_binary_with_convert(config3.value) + ] + end) + end + + test "subkeys with full update right merge", %{conn: conn} do + config1 = + insert(:config, + key: ":emoji", + value: ConfigDB.to_binary(groups: [a: 1, b: 2], key: [a: 1]) + ) + + config2 = + insert(:config, + key: ":assets", + value: ConfigDB.to_binary(mascots: [a: 1, b: 2], key: [a: 1]) + ) + + %{"configs" => configs} = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + vals = + Enum.filter(configs, fn %{"group" => group, "key" => key} -> + group == ":pleroma" and key in [config1.key, config2.key] + end) + + emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end) + assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end) + + emoji_val = ConfigDB.transform_with_out_binary(emoji["value"]) + assets_val = ConfigDB.transform_with_out_binary(assets["value"]) + + assert emoji_val[:groups] == [a: 1, b: 2] + assert assets_val[:mascots] == [a: 1, b: 2] + end + end + + test "POST /api/pleroma/admin/config error", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{"configs" => []}) + + assert json_response_and_validate_schema(conn, 400) == + %{"error" => "To use this endpoint you need to enable configuration from database."} + end + + describe "POST /api/pleroma/admin/config" do + setup do + http = Application.get_env(:pleroma, :http) + + on_exit(fn -> + Application.delete_env(:pleroma, :key1) + Application.delete_env(:pleroma, :key2) + Application.delete_env(:pleroma, :key3) + Application.delete_env(:pleroma, :key4) + Application.delete_env(:pleroma, :keyaa1) + Application.delete_env(:pleroma, :keyaa2) + Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal) + Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) + Application.put_env(:pleroma, :http, http) + Application.put_env(:tesla, :adapter, Tesla.Mock) + Restarter.Pleroma.refresh() + end) + end + + setup do: clear_config(:configurable_from_database, true) + + @tag capture_log: true + test "create new config setting in db", %{conn: conn} do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{ + group: ":ueberauth", + key: "Ueberauth", + value: [%{"tuple" => [":consumer_secret", "aaaa"]}] + }, + %{ + group: ":pleroma", + key: ":key2", + value: %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + } + }, + %{ + group: ":pleroma", + key: ":key3", + value: [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + }, + %{ + group: ":pleroma", + key: ":key4", + value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"} + }, + %{ + group: ":idna", + key: ":key5", + value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => "value1", + "db" => [":key1"] + }, + %{ + "group" => ":ueberauth", + "key" => "Ueberauth", + "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}], + "db" => [":consumer_secret"] + }, + %{ + "group" => ":pleroma", + "key" => ":key2", + "value" => %{ + ":nested_1" => "nested_value1", + ":nested_2" => [ + %{":nested_22" => "nested_value222"}, + %{":nested_33" => %{":nested_44" => "nested_444"}} + ] + }, + "db" => [":key2"] + }, + %{ + "group" => ":pleroma", + "key" => ":key3", + "value" => [ + %{"nested_3" => ":nested_3", "nested_33" => "nested_33"}, + %{"nested_4" => true} + ], + "db" => [":key3"] + }, + %{ + "group" => ":pleroma", + "key" => ":key4", + "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}, + "db" => [":key4"] + }, + %{ + "group" => ":idna", + "key" => ":key5", + "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}, + "db" => [":key5"] + } + ] + } + + assert Application.get_env(:pleroma, :key1) == "value1" + + assert Application.get_env(:pleroma, :key2) == %{ + nested_1: "nested_value1", + nested_2: [ + %{nested_22: "nested_value222"}, + %{nested_33: %{nested_44: "nested_444"}} + ] + } + + assert Application.get_env(:pleroma, :key3) == [ + %{"nested_3" => :nested_3, "nested_33" => "nested_33"}, + %{"nested_4" => true} + ] + + assert Application.get_env(:pleroma, :key4) == %{ + "endpoint" => "https://example.com", + nested_5: :upload + } + + assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} + end + + test "save configs setting without explicit key", %{conn: conn} do + level = Application.get_env(:quack, :level) + meta = Application.get_env(:quack, :meta) + webhook_url = Application.get_env(:quack, :webhook_url) + + on_exit(fn -> + Application.put_env(:quack, :level, level) + Application.put_env(:quack, :meta, meta) + Application.put_env(:quack, :webhook_url, webhook_url) + end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":quack", + key: ":level", + value: ":info" + }, + %{ + group: ":quack", + key: ":meta", + value: [":none"] + }, + %{ + group: ":quack", + key: ":webhook_url", + value: "https://hooks.slack.com/services/KEY" + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":quack", + "key" => ":level", + "value" => ":info", + "db" => [":level"] + }, + %{ + "group" => ":quack", + "key" => ":meta", + "value" => [":none"], + "db" => [":meta"] + }, + %{ + "group" => ":quack", + "key" => ":webhook_url", + "value" => "https://hooks.slack.com/services/KEY", + "db" => [":webhook_url"] + } + ] + } + + assert Application.get_env(:quack, :level) == :info + assert Application.get_env(:quack, :meta) == [:none] + assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" + end + + test "saving config with partial update", %{conn: conn} do + config = insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2)) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: config.group, key: config.key, value: [%{"tuple" => [":key3", 3]}]} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key2", 2]}, + %{"tuple" => [":key3", 3]} + ], + "db" => [":key1", ":key2", ":key3"] + } + ] + } + end + + test "saving config which need pleroma reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "update setting which need reboot, don't change reboot flag until reboot", %{conn: conn} do + chat = Config.get(:chat) + on_exit(fn -> Config.put(:chat, chat) end) + + assert conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/pleroma/admin/config", + %{ + configs: [ + %{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]} + ] + } + ) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "db" => [":enabled"], + "group" => ":pleroma", + "key" => ":chat", + "value" => [%{"tuple" => [":enabled", true]}] + } + ], + "need_reboot" => true + } + + assert conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key3", 3]}]} + ] + }) + |> json_response_and_validate_schema(200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key3", 3]} + ], + "db" => [":key3"] + } + ], + "need_reboot" => true + } + + capture_log(fn -> + assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == + %{} + end) =~ "pleroma restarted" + + configs = + conn + |> get("/api/pleroma/admin/config") + |> json_response_and_validate_schema(200) + + assert configs["need_reboot"] == false + end + + test "saving config with nested merge", %{conn: conn} do + config = + insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2])) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + value: [ + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key1", 1]}, + %{"tuple" => [":key3", 3]}, + %{ + "tuple" => [ + ":key2", + [ + %{"tuple" => [":k1", 1]}, + %{"tuple" => [":k2", 1]}, + %{"tuple" => [":k3", 3]} + ] + ] + } + ], + "db" => [":key1", ":key3", ":key2"] + } + ] + } + end + + test "saving special atoms", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{ + "tuple" => [ + ":ssl_options", + [%{"tuple" => [":versions", [":tlsv1", ":tlsv1.1", ":tlsv1.2"]]}] + ] + } + ], + "db" => [":ssl_options"] + } + ] + } + + assert Application.get_env(:pleroma, :key1) == [ + ssl_options: [versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]] + ] + end + + test "saving full setting if value is in full_key_update list", %{conn: conn} do + backends = Application.get_env(:logger, :backends) + on_exit(fn -> Application.put_env(:logger, :backends, backends) end) + + config = + insert(:config, + group: ":logger", + key: ":backends", + value: :erlang.term_to_binary([]) + ) + + Pleroma.Config.TransferTask.load_and_update_env([], false) + + assert Application.get_env(:logger, :backends) == [] + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + value: [":console"] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":logger", + "key" => ":backends", + "value" => [ + ":console" + ], + "db" => [":backends"] + } + ] + } + + assert Application.get_env(:logger, :backends) == [ + :console + ] + end + + test "saving full setting if value is not keyword", %{conn: conn} do + config = + insert(:config, + group: ":tesla", + key: ":adapter", + value: :erlang.term_to_binary(Tesla.Adapter.Hackey) + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: config.group, key: config.key, value: "Tesla.Adapter.Httpc"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":tesla", + "key" => ":adapter", + "value" => "Tesla.Adapter.Httpc", + "db" => [":adapter"] + } + ] + } + end + + test "update config setting & delete with fallback to default value", %{ + conn: conn, + admin: admin, + token: token + } do + ueberauth = Application.get_env(:ueberauth, Ueberauth) + config1 = insert(:config, key: ":keyaa1") + config2 = insert(:config, key: ":keyaa2") + + config3 = + insert(:config, + group: ":ueberauth", + key: "Ueberauth" + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: config1.group, key: config1.key, value: "another_value"}, + %{group: config2.group, key: config2.key, value: "another_value"} + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => config1.key, + "value" => "another_value", + "db" => [":keyaa1"] + }, + %{ + "group" => ":pleroma", + "key" => config2.key, + "value" => "another_value", + "db" => [":keyaa2"] + } + ] + } + + assert Application.get_env(:pleroma, :keyaa1) == "another_value" + assert Application.get_env(:pleroma, :keyaa2) == "another_value" + assert Application.get_env(:ueberauth, Ueberauth) == ConfigDB.from_binary(config3.value) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: config2.group, key: config2.key, delete: true}, + %{ + group: ":ueberauth", + key: "Ueberauth", + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [] + } + + assert Application.get_env(:ueberauth, Ueberauth) == ueberauth + refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2) + end + + test "common config example", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\/\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ] + } + ] + }) + + assert Config.get([Pleroma.Captcha.NotReal, :name]) == "Pleroma" + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Captcha.NotReal", + "value" => [ + %{"tuple" => [":enabled", false]}, + %{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]}, + %{"tuple" => [":seconds_valid", 60]}, + %{"tuple" => [":path", ""]}, + %{"tuple" => [":key1", nil]}, + %{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]}, + %{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]}, + %{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]}, + %{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]}, + %{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}, + %{"tuple" => [":name", "Pleroma"]} + ], + "db" => [ + ":enabled", + ":method", + ":seconds_valid", + ":path", + ":key1", + ":partial_chain", + ":regex1", + ":regex2", + ":regex3", + ":regex4", + ":name" + ] + } + ] + } + end + + test "tuples with more than two values", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => "Pleroma.Web.Endpoint.NotReal", + "value" => [ + %{ + "tuple" => [ + ":http", + [ + %{ + "tuple" => [ + ":key2", + [ + %{ + "tuple" => [ + ":_", + [ + %{ + "tuple" => [ + "/api/v1/streaming", + "Pleroma.Web.MastodonAPI.WebsocketHandler", + [] + ] + }, + %{ + "tuple" => [ + "/websocket", + "Phoenix.Endpoint.CowboyWebSocket", + %{ + "tuple" => [ + "Phoenix.Transports.WebSocket", + %{ + "tuple" => [ + "Pleroma.Web.Endpoint", + "Pleroma.Web.UserSocket", + [] + ] + } + ] + } + ] + }, + %{ + "tuple" => [ + ":_", + "Phoenix.Endpoint.Cowboy2Handler", + %{"tuple" => ["Pleroma.Web.Endpoint", []]} + ] + } + ] + ] + } + ] + ] + } + ] + ] + } + ], + "db" => [":http"] + } + ] + } + end + + test "settings with nesting map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000, + "nested" => %{ + ":max_options" => 20, + ":max_option_chars" => 200, + ":min_expiration" => 0, + ":max_expiration" => 31_536_000 + } + } + ] + } + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => [ + %{"tuple" => [":key2", "some_val"]}, + %{ + "tuple" => [ + ":key3", + %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0, + "nested" => %{ + ":max_expiration" => 31_536_000, + ":max_option_chars" => 200, + ":max_options" => 20, + ":min_expiration" => 0 + } + } + ] + } + ], + "db" => [":key2", ":key3"] + } + ] + } + end + + test "value as map", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"} + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == + %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":key1", + "value" => %{"key" => "some_val"}, + "db" => [":key1"] + } + ] + } + end + + test "queues key as atom", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":oban", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ], + "db" => [ + ":federator_incoming", + ":federator_outgoing", + ":web_push", + ":mailer", + ":transmogrifier", + ":scheduled_activities", + ":background" + ] + } + ] + } + end + + test "delete part of settings by atom subkeys", %{conn: conn} do + config = + insert(:config, + key: ":keyaa1", + value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") + ) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + subkeys: [":subkey1", ":subkey3"], + delete: true + } + ] + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":keyaa1", + "value" => [%{"tuple" => [":subkey2", "val2"]}], + "db" => [":subkey2"] + } + ] + } + end + + test "proxy tuple localhost", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple domain", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]} in value + assert ":proxy_url" in db + end + + test "proxy tuple ip", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{ + group: ":pleroma", + key: ":http", + value: [ + %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} + ] + } + ] + }) + + assert %{ + "configs" => [ + %{ + "group" => ":pleroma", + "key" => ":http", + "value" => value, + "db" => db + } + ] + } = json_response_and_validate_schema(conn, 200) + + assert %{"tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]} in value + assert ":proxy_url" in db + end + + @tag capture_log: true + test "doesn't set keys not in the whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :key1}, + {:pleroma, :key2}, + {:pleroma, Pleroma.Captcha.NotReal}, + {:not_real} + ]) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/config", %{ + configs: [ + %{group: ":pleroma", key: ":key1", value: "value1"}, + %{group: ":pleroma", key: ":key2", value: "value2"}, + %{group: ":pleroma", key: ":key3", value: "value3"}, + %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"}, + %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"}, + %{group: ":not_real", key: ":anything", value: "value6"} + ] + }) + + assert Application.get_env(:pleroma, :key1) == "value1" + assert Application.get_env(:pleroma, :key2) == "value2" + assert Application.get_env(:pleroma, :key3) == nil + assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil + assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5" + assert Application.get_env(:not_real, :anything) == "value6" + end + end + + describe "GET /api/pleroma/admin/config/descriptions" do + test "structure", %{conn: conn} do + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + assert [child | _others] = json_response_and_validate_schema(conn, 200) + + assert child["children"] + assert child["key"] + assert String.starts_with?(child["group"], ":") + assert child["description"] + end + + test "filters by database configuration whitelist", %{conn: conn} do + clear_config(:database_config_whitelist, [ + {:pleroma, :instance}, + {:pleroma, :activitypub}, + {:pleroma, Pleroma.Upload}, + {:esshd} + ]) + + admin = insert(:user, is_admin: true) + + conn = + assign(conn, :user, admin) + |> get("/api/pleroma/admin/config/descriptions") + + children = json_response_and_validate_schema(conn, 200) + + assert length(children) == 4 + + assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 + + instance = Enum.find(children, fn c -> c["key"] == ":instance" end) + assert instance["children"] + + activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end) + assert activitypub["children"] + + web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) + assert web_endpoint["children"] + + esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) + assert esshd["children"] + end + end +end diff --git a/test/web/admin_api/controllers/relay_controller_test.exs b/test/web/admin_api/controllers/relay_controller_test.exs new file mode 100644 index 000000000..64086adc5 --- /dev/null +++ b/test/web/admin_api/controllers/relay_controller_test.exs @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RelayControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + alias Pleroma.Config + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "relays" do + test "POST /relay", %{conn: conn, admin: admin} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + end + + test "GET /relay", %{conn: conn} do + relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + conn = get(conn, "/api/pleroma/admin/relay") + + assert json_response_and_validate_schema(conn, 200)["relays"] -- + ["mastodon.example.org", "mstdn.io"] == [] + end + + test "DELETE /relay", %{conn: conn, admin: admin} do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response_and_validate_schema(conn, 200) == + "http://mastodon.example.org/users/admin" + + [log_entry_one, log_entry_two] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry_one) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + + assert ModerationLog.get_log_entry_message(log_entry_two) == + "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" + end + end +end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 2291f76dd..6bd26050e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -5,7 +5,9 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat alias Pleroma.Conversation.Participation + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -23,6 +25,150 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "posting chat messages" do + setup do: clear_config([:instance, :chat_limit]) + + test "it posts a chat message without content but with an attachment" do + author = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id) + + with_mocks([ + { + Pleroma.Web.Streamer, + [], + [ + stream: fn _, _ -> + nil + end + ] + }, + { + Pleroma.Web.Push, + [], + [ + send: fn _ -> nil end + ] + } + ]) do + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + nil, + media_id: upload.id + ) + + notification = + Notification.for_user_and_activity(recipient, activity) + |> Repo.preload(:activity) + + assert called(Pleroma.Web.Push.send(notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) + assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_)) + + assert activity + end + end + + test "it adds html newlines" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "uguu\nuguuu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == "uguu
uguuu" + end + + test "it linkifies" do + author = insert(:user) + recipient = insert(:user) + + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "https://example.org is the site of @#{other_user.nickname} #2hu" + ) + + assert other_user.ap_id not in activity.recipients + + object = Object.normalize(activity, false) + + assert object.data["content"] == + "https://example.org is the site of @#{other_user.nickname} #2hu" + end + + test "it posts a chat message" do + author = insert(:user) + recipient = insert(:user) + + {:ok, activity} = + CommonAPI.post_chat_message( + author, + recipient, + "a test message :firefox:" + ) + + assert activity.data["type"] == "Create" + assert activity.local + object = Object.normalize(activity) + + assert object.data["type"] == "ChatMessage" + assert object.data["to"] == [recipient.ap_id] + + assert object.data["content"] == + "a test message <script>alert('uuu')</script> :firefox:" + + assert object.data["emoji"] == %{ + "firefox" => "http://localhost:4001/emoji/Firefox.gif" + } + + assert Chat.get(author.id, recipient.ap_id) + assert Chat.get(recipient.id, author.ap_id) + + assert :ok == Pleroma.Web.Federator.perform(:publish, activity) + end + + test "it reject messages over the local limit" do + Pleroma.Config.put([:instance, :chat_limit], 2) + + author = insert(:user) + recipient = insert(:user) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123" + ) + + assert message == :content_too_long + end + end + describe "unblocking" do test "it works even without an existing block activity" do blocked = insert(:user) diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index e278d61f5..698c99711 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -54,6 +54,27 @@ test "list of notifications" do assert response == expected_response end + test "by default, does not contain pleroma:chat_mention" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + result = + conn + |> get("/api/v1/notifications") + |> json_response_and_validate_schema(200) + + assert [] == result + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention") + |> json_response_and_validate_schema(200) + + assert [_] = result + end + test "getting a single notification" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 84d46895e..c605957b1 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -111,6 +111,44 @@ test "constructs hashtags from search query", %{conn: conn} do %{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"}, %{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"} ] + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}") + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"} + ] + + results = + conn + |> get( + "/api/v2/search?#{ + URI.encode_query(%{ + q: + "https://www.washingtonpost.com/sports/2020/06/10/" <> + "nascar-ban-display-confederate-flag-all-events-properties/" + }) + }" + ) + |> json_response_and_validate_schema(200) + + assert results["hashtags"] == [ + %{"name" => "nascar", "url" => "#{Web.base_url()}/tag/nascar"}, + %{"name" => "ban", "url" => "#{Web.base_url()}/tag/ban"}, + %{"name" => "display", "url" => "#{Web.base_url()}/tag/display"}, + %{"name" => "confederate", "url" => "#{Web.base_url()}/tag/confederate"}, + %{"name" => "flag", "url" => "#{Web.base_url()}/tag/flag"}, + %{"name" => "all", "url" => "#{Web.base_url()}/tag/all"}, + %{"name" => "events", "url" => "#{Web.base_url()}/tag/events"}, + %{"name" => "properties", "url" => "#{Web.base_url()}/tag/properties"}, + %{ + "name" => "NascarBanDisplayConfederateFlagAllEventsProperties", + "url" => + "#{Web.base_url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties" + } + ] end test "excludes a blocked users from search results", %{conn: conn} do diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs index 4aa260663..d36bb1ae8 100644 --- a/test/web/mastodon_api/controllers/subscription_controller_test.exs +++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -58,7 +58,9 @@ test "successful creation", %{conn: conn} do result = conn |> post("/api/v1/push/subscription", %{ - "data" => %{"alerts" => %{"mention" => true, "test" => true}}, + "data" => %{ + "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true} + }, "subscription" => @sub }) |> json_response_and_validate_schema(200) @@ -66,7 +68,7 @@ test "successful creation", %{conn: conn} do [subscription] = Pleroma.Repo.all(Subscription) assert %{ - "alerts" => %{"mention" => true}, + "alerts" => %{"mention" => true, "pleroma:chat_mention" => true}, "endpoint" => subscription.endpoint, "id" => to_string(subscription.id), "server_key" => @server_key diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index f91333e5c..044f088a4 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -72,6 +72,7 @@ test "Represent a user account" do fields: [] }, pleroma: %{ + ap_id: user.ap_id, background_image: "https://example.com/images/asuka_hospital.png", confirmation_pending: false, tags: [], @@ -148,6 +149,7 @@ test "Represent a Service(bot) account" do fields: [] }, pleroma: %{ + ap_id: user.ap_id, background_image: nil, confirmation_pending: false, tags: [], diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index f15be1df1..b2fa5b302 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -6,7 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.Notification + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -14,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView import Pleroma.Factory defp test_notifications_rendering(notifications, user, expected_result) do @@ -31,6 +35,30 @@ defp test_notifications_rendering(notifications, user, expected_result) do assert expected_result == result end + test "ChatMessage notification" do + user = insert(:user) + recipient = insert(:user) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude") + + {:ok, [notification]} = Notification.create_notifications(activity) + + object = Object.normalize(activity) + chat = Chat.get(recipient.id, user.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "pleroma:chat_mention", + account: AccountView.render("show.json", %{user: user, for: recipient}), + chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], recipient, [expected]) + end + test "Mention notification" do user = insert(:user) mentioned_user = insert(:user) diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index 9bcc07b37..00925caad 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -145,7 +145,8 @@ test "it shows default features flags", %{conn: conn} do "shareable_emoji_packs", "multifetch", "pleroma_emoji_reactions", - "pleroma:api/v1/notifications:include_types_filter" + "pleroma:api/v1/notifications:include_types_filter", + "pleroma_chat_messages" ] assert MapSet.subset?( diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs new file mode 100644 index 000000000..82e16741d --- /dev/null +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -0,0 +1,336 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do + setup do: oauth_access(["write:chats"]) + + test "it marks one message as read", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read") + |> json_response_and_validate_schema(200) + + assert result["unread"] == false + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/read" do + setup do: oauth_access(["write:chats"]) + + test "given a `last_read_id`, it marks everything until then as read", %{ + conn: conn, + user: user + } do + other_user = insert(:user) + + {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup") + {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2") + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + object = Object.normalize(create, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == true + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id}) + |> json_response_and_validate_schema(200) + + assert result["unread"] == 1 + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + assert cm_ref.unread == false + end + end + + describe "POST /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["write:chats"]) + + test "it posts a message to the chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"}) + |> json_response_and_validate_schema(200) + + assert result["content"] == "Hallo!!" + assert result["chat_id"] == chat.id |> to_string() + end + + test "it fails if there is no content", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(400) + + assert result + end + + test "it works with an attachment", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{ + "media_id" => to_string(upload.id) + }) + |> json_response_and_validate_schema(200) + + assert result["attachment"] + end + end + + describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do + setup do: oauth_access(["write:chats"]) + + test "it deletes a message from the chat", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, message} = + CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend") + + {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni") + + object = Object.normalize(message, false) + + chat = Chat.get(user.id, recipient.ap_id) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + # Deleting your own message removes the message and the reference + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) + + # Deleting other people's messages just removes the reference + object = Object.normalize(other_message, false) + cm_ref = MessageReference.for_chat_and_object(chat, object) + + result = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == cm_ref.id + refute MessageReference.get_by_id(cm_ref.id) + assert Object.get_by_id(object.id) + end + end + + describe "GET /api/v1/pleroma/chats/:id/messages" do + setup do: oauth_access(["read:chats"]) + + test "it paginates", %{conn: conn, user: user} do + recipient = insert(:user) + + Enum.each(1..30, fn _ -> + {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey") + end) + + chat = Chat.get(user.id, recipient.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + assert length(result) == 20 + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + |> json_response_and_validate_schema(200) + + assert length(result) == 10 + end + + test "it returns the messages for a given chat", %{conn: conn, user: user} do + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey") + {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?") + {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?") + + chat = Chat.get(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + |> json_response_and_validate_schema(200) + + result + |> Enum.each(fn message -> + assert message["chat_id"] == chat.id |> to_string() + end) + + assert length(result) == 3 + + # Trying to get the chat of a different user + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/chats/#{chat.id}/messages") + + assert result |> json_response(404) + end + end + + describe "POST /api/v1/pleroma/chats/by-account-id/:id" do + setup do: oauth_access(["write:chats"]) + + test "it creates or returns a chat", %{conn: conn} do + other_user = insert(:user) + + result = + conn + |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] + end + end + + describe "GET /api/v1/pleroma/chats/:id" do + setup do: oauth_access(["read:chats"]) + + test "it returns a chat", %{conn: conn, user: user} do + other_user = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats/#{chat.id}") + |> json_response_and_validate_schema(200) + + assert result["id"] == to_string(chat.id) + end + end + + describe "GET /api/v1/pleroma/chats" do + setup do: oauth_access(["read:chats"]) + + test "it does not return chats with users you blocked", %{conn: conn, user: user} do + recipient = insert(:user) + + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 1 + + User.block(user, recipient) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 0 + end + + test "it returns all chats", %{conn: conn, user: user} do + Enum.each(1..30, fn _ -> + recipient = insert(:user) + {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id) + end) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + assert length(result) == 30 + end + + test "it return a list of chats the current user is participating in, in descending order of updates", + %{conn: conn, user: user} do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id) + :timer.sleep(1000) + {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id) + :timer.sleep(1000) + {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id) + :timer.sleep(1000) + + # bump the second one + {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id) + + result = + conn + |> get("/api/v1/pleroma/chats") + |> json_response_and_validate_schema(200) + + ids = Enum.map(result, & &1["id"]) + + assert ids == [ + chat_2.id |> to_string(), + chat_3.id |> to_string(), + chat_1.id |> to_string() + ] + end + end +end diff --git a/test/web/pleroma_api/views/chat/message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs new file mode 100644 index 000000000..e5b165255 --- /dev/null +++ b/test/web/pleroma_api/views/chat/message_reference_view_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + + import Pleroma.Factory + + test "it displays a chat message" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:") + + chat = Chat.get(user.id, recipient.ap_id) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message[:id] == cm_ref.id + assert chat_message[:content] == "kippis :firefox:" + assert chat_message[:account_id] == user.id + assert chat_message[:chat_id] + assert chat_message[:created_at] + assert chat_message[:unread] == false + assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) + + {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id) + + object = Object.normalize(activity) + + cm_ref = MessageReference.for_chat_and_object(chat, object) + + chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + + assert chat_message_two[:id] == cm_ref.id + assert chat_message_two[:content] == "gkgkgk" + assert chat_message_two[:account_id] == recipient.id + assert chat_message_two[:chat_id] == chat_message[:chat_id] + assert chat_message_two[:attachment] + assert chat_message_two[:unread] == true + end +end diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs new file mode 100644 index 000000000..14eecb1bd --- /dev/null +++ b/test/web/pleroma_api/views/chat_view_test.exs @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ChatViewTest do + use Pleroma.DataCase + + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + alias Pleroma.Web.PleromaAPI.ChatView + + import Pleroma.Factory + + test "it represents a chat" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + assert represented_chat == %{ + id: "#{chat.id}", + account: AccountView.render("show.json", user: recipient), + unread: 0, + last_message: nil, + updated_at: Utils.to_masto_date(chat.updated_at) + } + + {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello") + + chat_message = Object.normalize(chat_message_creation, false) + + {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id) + + represented_chat = ChatView.render("show.json", chat: chat) + + cm_ref = MessageReference.for_chat_and_object(chat, chat_message) + + assert represented_chat[:last_message] == + MessageReferenceView.render("show.json", chat_message_reference: cm_ref) + end +end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index a826b24c9..b48952b29 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -5,8 +5,10 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription @@ -60,7 +62,8 @@ test "performs sending notifications" do notif = insert(:notification, user: user, - activity: activity + activity: activity, + type: "mention" ) assert Impl.perform(notif) == {:ok, [:ok, :ok]} @@ -126,7 +129,7 @@ test "renders title and body for create activity" do ) == "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "mention"}) == "New Mention" end @@ -136,9 +139,10 @@ test "renders title and body for follow activity" do {:ok, _, _, activity} = CommonAPI.follow(user, other_user) object = Object.normalize(activity, false) - assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you" + assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) == + "@Bob has followed you" - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "follow"}) == "New Follower" end @@ -157,7 +161,7 @@ test "renders title and body for announce activity" do assert Impl.format_body(%{activity: announce_activity}, user, object) == "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." - assert Impl.format_title(%{activity: announce_activity}) == + assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) == "New Repeat" end @@ -173,9 +177,10 @@ test "renders title and body for like activity" do {:ok, activity} = CommonAPI.favorite(user, activity.id) object = Object.normalize(activity) - assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post" + assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) == + "@Bob has favorited your post" - assert Impl.format_title(%{activity: activity}) == + assert Impl.format_title(%{activity: activity, type: "favourite"}) == "New Favorite" end @@ -193,6 +198,46 @@ test "renders title for create activity with direct visibility" do end describe "build_content/3" do + test "builds content for chat messages" do + user = insert(:user) + recipient = insert(:user) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey") + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: hey", + title: "New Chat Message" + } + end + + test "builds content for chat messages with no content" do + user = insert(:user) + recipient = insert(:user) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) + object = Object.normalize(chat, false) + [notification] = Notification.for_user(recipient) + + res = Impl.build_content(notification, user, object) + + assert res == %{ + body: "@#{user.nickname}: (Attachment)", + title: "New Chat Message" + } + end + test "hides details for notifications when privacy option enabled" do user = insert(:user, nickname: "Bob") user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) @@ -218,7 +263,7 @@ test "hides details for notifications when privacy option enabled" do status: "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." }) - notif = insert(:notification, user: user2, activity: activity) + notif = insert(:notification, user: user2, activity: activity, type: "mention") actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) object = Object.normalize(activity) @@ -281,7 +326,7 @@ test "returns regular content for notifications with privacy option disabled" do {:ok, activity} = CommonAPI.favorite(user, activity.id) - notif = insert(:notification, user: user2, activity: activity) + notif = insert(:notification, user: user2, activity: activity, type: "favourite") actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) object = Object.normalize(activity) diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 3f012259a..245f6e63f 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -7,11 +7,15 @@ defmodule Pleroma.Web.StreamerTest do import Pleroma.Factory + alias Pleroma.Chat + alias Pleroma.Chat.MessageReference alias Pleroma.Conversation.Participation alias Pleroma.List + alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Streamer + alias Pleroma.Web.StreamerView @moduletag needs_streamer: true, capture_log: true @@ -145,6 +149,57 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif refute Streamer.filtered_by_user?(user, notify) end + test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + Streamer.get_topic_and_add_socket("user:pleroma_chat", user) + Streamer.stream("user:pleroma_chat", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat messages to the 'user' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno") + object = Object.normalize(create_activity, false) + chat = Chat.get(user.id, other_user.ap_id) + cm_ref = MessageReference.for_chat_and_object(chat, object) + cm_ref = %{cm_ref | chat: chat, object: object} + + Streamer.get_topic_and_add_socket("user", user) + Streamer.stream("user", {user, cm_ref}) + + text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + + assert text =~ "hey cirno" + assert_receive {:text, ^text} + end + + test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do + other_user = insert(:user) + + {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey") + + notify = + Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id) + |> Repo.preload(:activity) + + Streamer.get_topic_and_add_socket("user:notification", user) + Streamer.stream("user:notification", notify) + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) + end + test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ user: user } do diff --git a/test/workers/cron/purge_expired_activities_worker_test.exs b/test/workers/cron/purge_expired_activities_worker_test.exs index 5864f9e5f..6d2991a60 100644 --- a/test/workers/cron/purge_expired_activities_worker_test.exs +++ b/test/workers/cron/purge_expired_activities_worker_test.exs @@ -11,7 +11,10 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorkerTest do import Pleroma.Factory import ExUnit.CaptureLog - setup do: clear_config([ActivityExpiration, :enabled]) + setup do + clear_config([ActivityExpiration, :enabled]) + clear_config([:instance, :rewrite_policy]) + end test "deletes an expiration activity" do Pleroma.Config.put([ActivityExpiration, :enabled], true) @@ -36,6 +39,35 @@ test "deletes an expiration activity" do refute Pleroma.Repo.get(Pleroma.ActivityExpiration, expiration.id) end + test "works with ActivityExpirationPolicy" do + Pleroma.Config.put([ActivityExpiration, :enabled], true) + + Pleroma.Config.put( + [:instance, :rewrite_policy], + Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy + ) + + user = insert(:user) + + days = Pleroma.Config.get([:mrf_activity_expiration, :days], 365) + + {:ok, %{id: id} = activity} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"}) + + past_date = + NaiveDateTime.utc_now() |> Timex.shift(days: -days) |> NaiveDateTime.truncate(:second) + + activity + |> Repo.preload(:expiration) + |> Map.get(:expiration) + |> Ecto.Changeset.change(%{scheduled_at: past_date}) + |> Repo.update!() + + Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker.perform(:ops, :pid) + + assert [%{data: %{"type" => "Delete", "deleted_activity_id" => ^id}}] = + Pleroma.Repo.all(Pleroma.Activity) + end + describe "delete_activity/1" do test "adds log message if activity isn't find" do assert capture_log([level: :error], fn ->