Merge branch 'develop' into preload-data

This commit is contained in:
Mark Felder 2020-06-12 16:52:52 -05:00
commit 26f710b9e3
141 changed files with 6954 additions and 2512 deletions

View file

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [unreleased] ## [unreleased]
### Changed ### Changed
- MFR policy to set global expiration for all local Create activities
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- **Breaking:** Emoji API: changed methods and renamed routes. - **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. - **Breaking:** removed `with_move` parameter from notifications timeline.
### Added ### Added
- Chats: Added support for federated chats. For details, see the docs.
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance` - Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information. - 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). - 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 wont start. For hackney OTP update is not required. - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App. - Mix task to create trusted OAuth App.
- Mix task to reset MFA for user accounts
- Notifications: Added `follow_request` notification type. - Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy - Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - 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 - Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view. - Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs
</details> </details>
### Fixed ### Fixed

View file

@ -52,12 +52,12 @@ defp render_views(user) do
defp opts_for_home_timeline(user) do defp opts_for_home_timeline(user) do
%{ %{
"blocking_user" => user, blocking_user: user,
"count" => "20", count: "20",
"muting_user" => user, muting_user: user,
"type" => ["Create", "Announce"], type: ["Create", "Announce"],
"user" => user, user: user,
"with_muted" => "true" with_muted: true
} }
end end
@ -70,17 +70,17 @@ defp fetch_home_timeline(user) do
ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last() ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last()
second_page_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() |> Enum.reverse()
|> List.last() |> List.last()
third_page_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() |> Enum.reverse()
|> List.last() |> List.last()
forth_page_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() |> Enum.reverse()
|> List.last() |> List.last()
@ -90,19 +90,19 @@ defp fetch_home_timeline(user) do
}, },
inputs: %{ inputs: %{
"1 page" => opts, "1 page" => opts,
"2 page" => Map.put(opts, "max_id", first_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), "3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, "max_id", third_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), "5 page" => Map.put(opts, :max_id, forth_page_last.id),
"1 page only media" => Map.put(opts, "only_media", "true"), "1 page only media" => Map.put(opts, :only_media, true),
"2 page only media" => "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" => "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" => "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" => "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() formatters: formatters()
) )
@ -110,12 +110,12 @@ defp fetch_home_timeline(user) do
defp opts_for_direct_timeline(user) do defp opts_for_direct_timeline(user) do
%{ %{
:visibility => "direct", visibility: "direct",
"blocking_user" => user, blocking_user: user,
"count" => "20", count: "20",
"type" => "Create", type: "Create",
"user" => user, user: user,
"with_muted" => "true" with_muted: true
} }
end end
@ -130,7 +130,7 @@ defp fetch_direct_timeline(user) do
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts)
|> List.last() |> 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 = second_page_last =
recipients recipients
@ -138,7 +138,7 @@ defp fetch_direct_timeline(user) do
|> Pagination.fetch_paginated(opts2) |> Pagination.fetch_paginated(opts2)
|> List.last() |> 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 = third_page_last =
recipients recipients
@ -146,7 +146,7 @@ defp fetch_direct_timeline(user) do
|> Pagination.fetch_paginated(opts3) |> Pagination.fetch_paginated(opts3)
|> List.last() |> 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 = forth_page_last =
recipients recipients
@ -165,7 +165,7 @@ defp fetch_direct_timeline(user) do
"2 page" => opts2, "2 page" => opts2,
"3 page" => opts3, "3 page" => opts3,
"4 page" => opts4, "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() formatters: formatters()
) )
@ -173,34 +173,34 @@ defp fetch_direct_timeline(user) do
defp opts_for_public_timeline(user) do defp opts_for_public_timeline(user) do
%{ %{
"type" => ["Create", "Announce"], type: ["Create", "Announce"],
"local_only" => false, local_only: false,
"blocking_user" => user, blocking_user: user,
"muting_user" => user muting_user: user
} }
end end
defp opts_for_public_timeline(user, :local) do defp opts_for_public_timeline(user, :local) do
%{ %{
"type" => ["Create", "Announce"], type: ["Create", "Announce"],
"local_only" => true, local_only: true,
"blocking_user" => user, blocking_user: user,
"muting_user" => user muting_user: user
} }
end end
defp opts_for_public_timeline(user, :tag) do defp opts_for_public_timeline(user, :tag) do
%{ %{
"blocking_user" => user, blocking_user: user,
"count" => "20", count: "20",
"local_only" => nil, local_only: nil,
"muting_user" => user, muting_user: user,
"tag" => ["tag"], tag: ["tag"],
"tag_all" => [], tag_all: [],
"tag_reject" => [], tag_reject: [],
"type" => "Create", type: "Create",
"user" => user, user: user,
"with_muted" => "true" with_muted: true
} }
end end
@ -223,7 +223,7 @@ defp fetch_public_timeline(user, :tag) do
end end
defp fetch_public_timeline(user, :only_media) do 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") fetch_public_timeline(opts, "public timeline only media")
end end
@ -245,15 +245,13 @@ defp fetch_public_timeline(user, :with_blocks) do
user = User.get_by_id(user.id) user = User.get_by_id(user.id)
opts = Map.put(opts, "blocking_user", user) opts = Map.put(opts, :blocking_user, user)
Benchee.run( Benchee.run(%{
%{
"public timeline with user block" => fn -> "public timeline with user block" => fn ->
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
end end
}, })
)
domains = domains =
Enum.reduce(remote_non_friends, [], fn non_friend, domains -> Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
@ -269,30 +267,28 @@ defp fetch_public_timeline(user, :with_blocks) do
end) end)
user = User.get_by_id(user.id) user = User.get_by_id(user.id)
opts = Map.put(opts, "blocking_user", user) opts = Map.put(opts, :blocking_user, user)
Benchee.run( Benchee.run(%{
%{ "public timeline with domain block" => fn ->
"public timeline with domain block" => fn opts ->
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
end end
} })
)
end end
defp fetch_public_timeline(opts, title) when is_binary(title) do defp fetch_public_timeline(opts, title) when is_binary(title) do
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last() first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()
second_page_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() |> List.last()
third_page_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() |> List.last()
forth_page_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() |> List.last()
Benchee.run( Benchee.run(
@ -303,17 +299,17 @@ defp fetch_public_timeline(opts, title) when is_binary(title) do
}, },
inputs: %{ inputs: %{
"1 page" => opts, "1 page" => opts,
"2 page" => Map.put(opts, "max_id", first_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), "3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, "max_id", third_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) "5 page" => Map.put(opts, :max_id, forth_page_last.id)
}, },
formatters: formatters() formatters: formatters()
) )
end end
defp opts_for_notifications do defp opts_for_notifications do
%{"count" => "20", "with_muted" => "true"} %{count: "20", with_muted: true}
end end
defp fetch_notifications(user) do defp fetch_notifications(user) do
@ -322,15 +318,15 @@ defp fetch_notifications(user) do
first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last() first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last()
second_page_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() |> List.last()
third_page_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() |> List.last()
forth_page_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() |> List.last()
Benchee.run( Benchee.run(
@ -341,10 +337,10 @@ defp fetch_notifications(user) do
}, },
inputs: %{ inputs: %{
"1 page" => opts, "1 page" => opts,
"2 page" => Map.put(opts, "max_id", first_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), "3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, "max_id", third_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) "5 page" => Map.put(opts, :max_id, forth_page_last.id)
}, },
formatters: formatters() formatters: formatters()
) )
@ -354,13 +350,13 @@ defp fetch_favourites(user) do
first_page_last = ActivityPub.fetch_favourites(user) |> List.last() first_page_last = ActivityPub.fetch_favourites(user) |> List.last()
second_page_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 = 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 = 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( Benchee.run(
%{ %{
@ -370,10 +366,10 @@ defp fetch_favourites(user) do
}, },
inputs: %{ inputs: %{
"1 page" => %{}, "1 page" => %{},
"2 page" => %{"max_id" => first_page_last.id}, "2 page" => %{:max_id => first_page_last.id},
"3 page" => %{"max_id" => second_page_last.id}, "3 page" => %{:max_id => second_page_last.id},
"4 page" => %{"max_id" => third_page_last.id}, "4 page" => %{:max_id => third_page_last.id},
"5 page" => %{"max_id" => forth_page_last.id} "5 page" => %{:max_id => forth_page_last.id}
}, },
formatters: formatters() formatters: formatters()
) )
@ -381,8 +377,8 @@ defp fetch_favourites(user) do
defp opts_for_long_thread(user) do defp opts_for_long_thread(user) do
%{ %{
"blocking_user" => user, blocking_user: user,
"user" => user user: user
} }
end end
@ -392,9 +388,9 @@ defp fetch_long_thread(user) do
opts = opts_for_long_thread(user) 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( Benchee.run(
%{ %{
@ -514,13 +510,13 @@ defp render_long_thread(user) do
public_context = public_context =
ActivityPub.fetch_activities_for_context( ActivityPub.fetch_activities_for_context(
public.data["context"], public.data["context"],
Map.put(fetch_opts, "exclude_id", public.id) Map.put(fetch_opts, :exclude_id, public.id)
) )
private_context = private_context =
ActivityPub.fetch_activities_for_context( ActivityPub.fetch_activities_for_context(
private.data["context"], private.data["context"],
Map.put(fetch_opts, "exclude_id", private.id) Map.put(fetch_opts, :exclude_id, private.id)
) )
Benchee.run( Benchee.run(
@ -551,14 +547,14 @@ defp fetch_timelines_with_reply_filtering(user) do
end, end,
"Public timeline with reply filtering - following" => fn -> "Public timeline with reply filtering - following" => fn ->
public_params public_params
|> Map.put("reply_visibility", "following") |> Map.put(:reply_visibility, "following")
|> Map.put("reply_filtering_user", user) |> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
end, end,
"Public timeline with reply filtering - self" => fn -> "Public timeline with reply filtering - self" => fn ->
public_params public_params
|> Map.put("reply_visibility", "self") |> Map.put(:reply_visibility, "self")
|> Map.put("reply_filtering_user", user) |> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
end end
}, },
@ -577,16 +573,16 @@ defp fetch_timelines_with_reply_filtering(user) do
"Home timeline with reply filtering - following" => fn -> "Home timeline with reply filtering - following" => fn ->
private_params = private_params =
private_params private_params
|> Map.put("reply_filtering_user", user) |> Map.put(:reply_filtering_user, user)
|> Map.put("reply_visibility", "following") |> Map.put(:reply_visibility, "following")
ActivityPub.fetch_activities(recipients, private_params) ActivityPub.fetch_activities(recipients, private_params)
end, end,
"Home timeline with reply filtering - self" => fn -> "Home timeline with reply filtering - self" => fn ->
private_params = private_params =
private_params private_params
|> Map.put("reply_filtering_user", user) |> Map.put(:reply_filtering_user, user)
|> Map.put("reply_visibility", "self") |> Map.put(:reply_visibility, "self")
ActivityPub.fetch_activities(recipients, private_params) ActivityPub.fetch_activities(recipients, private_params)
end end

View file

@ -100,14 +100,14 @@ defp hashtag_fetching(params, user, local_only) do
_activities = _activities =
params params
|> Map.put("type", "Create") |> Map.put(:type, "Create")
|> Map.put("local_only", local_only) |> Map.put(:local_only, local_only)
|> Map.put("blocking_user", user) |> Map.put(:blocking_user, user)
|> Map.put("muting_user", user) |> Map.put(:muting_user, user)
|> Map.put("user", user) |> Map.put(:user, user)
|> Map.put("tag", tags) |> Map.put(:tag, tags)
|> Map.put("tag_all", tag_all) |> Map.put(:tag_all, tag_all)
|> Map.put("tag_reject", tag_reject) |> Map.put(:tag_reject, tag_reject)
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end end
end end

View file

@ -371,6 +371,8 @@
config :pleroma, :mrf_subchain, match_actor: %{} config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365
config :pleroma, :mrf_vocabulary, config :pleroma, :mrf_vocabulary,
accept: [], accept: [],
reject: [] reject: []

View file

@ -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, group: :pleroma,
key: :mrf_subchain, key: :mrf_subchain,

248
docs/API/chats.md Normal file
View file

@ -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.

View file

@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications - `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.

View file

@ -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 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). 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.

View file

@ -135,6 +135,16 @@ mix pleroma.user reset_password <nickname>
``` ```
## Disable Multi Factor Authentication (MFA/2FA) for a user
```sh tab="OTP"
./bin/pleroma_ctl user reset_mfa <nickname>
```
```sh tab="From Source"
mix pleroma.user reset_mfa <nickname>
```
## Set the value of the given user's settings ## Set the value of the given user's settings
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user set <nickname> [option ...] ./bin/pleroma_ctl user set <nickname> [option ...]

35
docs/ap_extensions.md Normal file
View file

@ -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`.

View file

@ -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: * `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default). * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesnt modify activities (default).
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt makes sense to use in production. * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt 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.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.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)). * `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.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.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)). * `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. * `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``. * `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). * `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 * `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk * `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 ### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed * `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances * `outgoing_blocks`: Whether to federate blocks to other instances

View file

@ -37,18 +37,17 @@ server {
listen 443 ssl http2; listen 443 ssl http2;
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_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# Add TLSv1.0 to support older devices ssl_protocols TLSv1.2 TLSv1.3;
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_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_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, # 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. # leave only prime256v1 or comment out the following line.
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;

View file

@ -237,6 +237,12 @@ def run(["gen-pack" | args]) do
end end
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 defp fetch_and_decode(from) do
with {:ok, json} <- fetch(from) do with {:ok, json} <- fetch(from) do
Jason.decode!(json) Jason.decode!(json)

View file

@ -144,6 +144,18 @@ def run(["reset_password", nickname]) do
end end
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 def run(["deactivate", nickname]) do
start_pleroma() start_pleroma()

View file

@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} @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 schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -300,32 +290,6 @@ def follow_accepted?(
def follow_accepted?(_), do: false 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, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: [] def all_by_actor_and_id(_actor, []), do: []

View file

@ -92,10 +92,10 @@ def handle_command(state, "home") do
params = params =
%{} %{}
|> Map.put("type", ["Create"]) |> Map.put(:type, ["Create"])
|> Map.put("blocking_user", user) |> Map.put(:blocking_user, user)
|> Map.put("muting_user", user) |> Map.put(:muting_user, user)
|> Map.put("user", user) |> Map.put(:user, user)
activities = activities =
[user.ap_id | Pleroma.User.following(user)] [user.ap_id | Pleroma.User.following(user)]

72
lib/pleroma/chat.ex Normal file
View file

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,117 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -163,8 +163,8 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
|> Enum.map(fn participation -> |> Enum.map(fn participation ->
activity_id = activity_id =
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user, user: user,
"blocking_user" => user blocking_user: user
}) })
%{ %{

View file

@ -141,6 +141,12 @@ def following_query(%User{} = user) do
|> where([r], r.state == ^:follow_accept) |> where([r], r.state == ^:follow_accept)
end 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 def following(%User{} = user) do
following = following =
following_query(user) following_query(user)

View file

@ -17,14 +17,6 @@ def append_uri_params(uri, appended_params) do
|> URI.to_string() |> URI.to_string()
end 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: Path.join([base, uri])
def maybe_add_base(uri, _base), do: uri def maybe_add_base(uri, _base), do: uri
end end

15
lib/pleroma/maps.ex Normal file
View file

@ -0,0 +1,15 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,85 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -30,12 +30,29 @@ defmodule Pleroma.Notification do
schema "notifications" do schema "notifications" do
field(:seen, :boolean, default: false) 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(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps() timestamps()
end 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() @spec unread_notifications_count(User.t()) :: integer()
def unread_notifications_count(%User{id: user_id}) do def unread_notifications_count(%User{id: user_id}) do
from(q in __MODULE__, from(q in __MODULE__,
@ -44,9 +61,21 @@ def unread_notifications_count(%User{id: user_id}) do
|> Repo.aggregate(:count, :id) |> Repo.aggregate(:count, :id)
end end
@notification_types ~w{
favourite
follow
follow_request
mention
move
pleroma:chat_mention
pleroma:emoji_reaction
reblog
}
def changeset(%Notification{} = notification, attrs) do def changeset(%Notification{} = notification, attrs) do
notification notification
|> cast(attrs, [:seen]) |> cast(attrs, [:seen, :type])
|> validate_inclusion(:type, @notification_types)
end end
@spec last_read_query(User.t()) :: Ecto.Queryable.t() @spec last_read_query(User.t()) :: Ecto.Queryable.t()
@ -300,42 +329,95 @@ def dismiss(%{id: user_id} = _user, id) do
end end
end end
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do def create_notifications(activity, options \\ [])
object = Object.normalize(activity)
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
object = Object.normalize(activity, false)
if object && object.data["type"] == "Answer" do if object && object.data["type"] == "Answer" do
{:ok, []} {:ok, []}
else else
do_create_notifications(activity) do_create_notifications(activity, options)
end end
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 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity) do_create_notifications(activity, options)
end 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) {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers potential_receivers = enabled_receivers ++ disabled_receivers
notifications = notifications =
Enum.map(potential_receivers, fn user -> 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) create_notification(activity, user, do_send)
end) end)
{:ok, notifications} {:ok, notifications}
end 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. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do unless skip?(activity, user) do
{:ok, %{notification: notification}} = {:ok, %{notification: notification}} =
Multi.new() 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") |> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction() |> Repo.transaction()
@ -527,4 +609,12 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
end end
def skip?(_, _, _), do: false 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 end

View file

@ -23,12 +23,12 @@ def page_keys, do: @page_keys
@spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()] @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, 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 = Repo.aggregate(query, :count, :id)
%{ %{
total: total, 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 end
@ -41,7 +41,7 @@ def fetch_paginated(query, params, :keyset, table_binding) do
|> enforce_order(options) |> enforce_order(options)
end end
def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do
total = total =
query query
|> Ecto.Query.exclude(:left_join) |> Ecto.Query.exclude(:left_join)
@ -49,7 +49,7 @@ def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding)
%{ %{
total: total, 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 end
@ -90,12 +90,6 @@ defp cast_params(params) do
skip_order: :boolean 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 = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes changeset.changes
end end

View file

@ -113,6 +113,10 @@ defp get_proxy_and_attachment_sources do
add_source(acc, host) add_source(acc, host)
end) 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 = upload_base_url =
if Config.get([Pleroma.Upload, :base_url]), if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host 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 do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
[] []
|> add_source(media_proxy_base_url)
|> add_source(upload_base_url) |> add_source(upload_base_url)
|> add_source(s3_endpoint) |> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist) |> add_source(media_proxy_whitelist)

View file

@ -67,6 +67,7 @@ def store(upload, opts \\ []) do
{:ok, {:ok,
%{ %{
"type" => opts.activity_type, "type" => opts.activity_type,
"mediaType" => upload.content_type,
"url" => [ "url" => [
%{ %{
"type" => "Link", "type" => "Link",

View file

@ -1489,6 +1489,8 @@ def perform(:delete, %User{} = user) do
delete_user_activities(user) delete_user_activities(user)
delete_outgoing_pending_follow_requests(user)
delete_or_deactivate(user) delete_or_deactivate(user)
end end
@ -1611,6 +1613,12 @@ defp delete_activity(%{data: %{"type" => type}} = activity, user)
defp delete_activity(_activity, _user), do: "Doing nothing" 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 def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
end end

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.FederatingPlug alias Pleroma.Web.FederatingPlug
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@ -230,27 +231,23 @@ def outbox(
when page? in [true, "true"] do when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
activities = # "include_poll_votes" is a hack because postgres generates inefficient
if params["max_id"] do # queries when filtering by 'Answer', poll votes will be hidden by the
ActivityPub.fetch_user_activities(user, for_user, %{ # visibility filter in this case anyway
"max_id" => params["max_id"], params =
# This is a hack because postgres generates inefficient queries when filtering by params
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway |> Map.drop(["nickname", "page"])
"include_poll_votes" => true, |> Map.put("include_poll_votes", true)
"limit" => 10 |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
})
else activities = ActivityPub.fetch_user_activities(user, for_user, params)
ActivityPub.fetch_user_activities(user, for_user, %{
"limit" => 10,
"include_poll_votes" => true
})
end
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
|> render("activity_collection_page.json", %{ |> render("activity_collection_page.json", %{
activities: activities, activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/outbox" iri: "#{user.ap_id}/outbox"
}) })
end end
@ -353,21 +350,24 @@ def read_inbox(
%{"nickname" => nickname, "page" => page?} = params %{"nickname" => nickname, "page" => page?} = params
) )
when page? in [true, "true"] do 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 = activities =
if params["max_id"] do [user.ap_id | User.following(user)]
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ |> ActivityPub.fetch_activities(params)
"max_id" => params["max_id"], |> Enum.reverse()
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
end
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> put_view(UserView) |> put_view(UserView)
|> render("activity_collection_page.json", %{ |> render("activity_collection_page.json", %{
activities: activities, activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/inbox" iri: "#{user.ap_id}/inbox"
}) })
end end

View file

@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
This module encodes our addressing policies and general shape of our objects. This module encodes our addressing policies and general shape of our objects.
""" """
alias Pleroma.Emoji
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
@ -65,6 +66,42 @@ def delete(actor, object_id) do
}, []} }, []}
end 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()} @spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do def tombstone(actor, id) do
{:ok, {:ok,

View file

@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(policies, %{} = object) do def filter(policies, %{} = object) do
policies policies
|> Enum.reduce({:ok, object}, fn |> Enum.reduce({:ok, object}, fn
policy, {:ok, object} -> policy, {:ok, object} -> policy.filter(object)
policy.filter(object) _, error -> error
_, error ->
error
end) end)
end end

View file

@ -0,0 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator 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.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
@ -43,8 +45,20 @@ def validate(%{"type" => "Delete"} = object, meta) do
def validate(%{"type" => "Like"} = object, meta) do def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do object
object = stringify_keys(object |> Map.from_struct()) |> 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} {:ok, object, meta}
end end
end end
@ -59,6 +73,18 @@ def validate(%{"type" => "EmojiReact"} = object, meta) do
end end
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 def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
@ -69,17 +95,30 @@ def validate(%{"type" => "Announce"} = object, meta) do
end end
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 def stringify_keys(%{__struct__: _} = object) do
object object
|> Map.from_struct() |> Map.from_struct()
|> stringify_keys |> stringify_keys
end end
def stringify_keys(object) do def stringify_keys(object) when is_map(object) do
object object
|> Map.new(fn {key, val} -> {to_string(key), val} end) |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
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 def fetch_actor(object) do
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
User.get_or_fetch_by_ap_id(actor) User.get_or_fetch_by_ap_id(actor)

View file

@ -0,0 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,123 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,91 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -46,12 +46,13 @@ def add_deleted_activity_id(cng) do
Answer Answer
Article Article
Audio Audio
ChatMessage
Event Event
Note Note
Page Page
Question Question
Video
Tombstone Tombstone
Video
} }
def validate_data(cng) do def validate_data(cng) do
cng cng

View file

@ -11,11 +11,13 @@ def cast(object) when is_binary(object) do
def cast(data) when is_list(data) do def cast(data) when is_list(data) do
data data
|> Enum.reduce({:ok, []}, fn element, acc -> |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
case {acc, ObjectID.cast(element)} do case ObjectID.cast(element) do
{:error, _} -> :error {:ok, id} ->
{_, :error} -> :error {:cont, {:ok, [id | list]}}
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
_ ->
{:halt, :error}
end end
end) end)
end end

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -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

View file

@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) 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} -> {:ok, value} ->
value value

View file

@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on. collection, and so on.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
def handle(object, meta \\ []) def handle(object, meta \\ [])
@ -27,6 +32,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, object, meta} {:ok, object, meta}
end 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: # Tasks this handles:
# - Add announce to object # - Add announce to object
# - Set up notification # - Set up notification
@ -88,6 +111,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
Object.decrease_replies_count(in_reply_to) Object.decrease_replies_count(in_reply_to)
end end
MessageReference.delete_for_object(deleted_object)
ActivityPub.stream_out(object) ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user) ActivityPub.stream_out_participations(deleted_object, user)
:ok :ok
@ -112,6 +137,39 @@ def handle(object, meta) do
{:ok, object, meta} {:ok, object, meta}
end 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 def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object), {:ok, _} <- Utils.remove_like_from_object(object, liked_object),
@ -148,4 +206,43 @@ def handle_undoing(
end end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]} 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 end

View file

@ -9,6 +9,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.EarmarkRenderer alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
@ -208,12 +210,6 @@ def fix_context(object) do
|> Map.put("conversation", context) |> Map.put("conversation", context)
end 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 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments = attachments =
Enum.map(attachment, fn data -> Enum.map(attachment, fn data ->
@ -226,9 +222,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
media_type = media_type =
cond do cond do
is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"] is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
is_binary(data["mediaType"]) -> data["mediaType"] MIME.valid?(data["mediaType"]) -> data["mediaType"]
is_binary(data["mimeType"]) -> data["mimeType"] MIME.valid?(data["mimeType"]) -> data["mimeType"]
true -> nil true -> nil
end end
@ -241,13 +237,13 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
attachment_url = attachment_url =
%{"href" => href} %{"href" => href}
|> add_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> add_if_present("type", Map.get(url || %{}, "type")) |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]} %{"url" => [attachment_url]}
|> add_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> add_if_present("type", data["type"]) |> Maps.put_if_present("type", data["type"])
|> add_if_present("name", data["name"]) |> Maps.put_if_present("name", data["name"])
end) end)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
@ -532,7 +528,8 @@ def handle_incoming(
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <- {:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => 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]), with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)}, {_, false} <- {:user_locked, User.locked?(followed)},
@ -575,6 +572,7 @@ def handle_incoming(
:noop :noop
end end
ActivityPub.notify_and_stream(activity)
{:ok, activity} {:ok, activity}
else else
_e -> _e ->
@ -595,6 +593,8 @@ def handle_incoming(
User.update_follower_count(followed) User.update_follower_count(followed)
User.update_following_count(follower) User.update_following_count(follower)
Notification.update_notification_type(followed, follow_activity)
ActivityPub.accept(%{ ActivityPub.accept(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
type: "Accept", type: "Accept",
@ -662,6 +662,16 @@ def handle_incoming(
|> handle_incoming(options) |> handle_incoming(options)
end 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) def handle_incoming(%{"type" => type} = data, _options)
when type in ["Like", "EmojiReact", "Announce"] do when type in ["Like", "EmojiReact", "Announce"] do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
@ -1113,6 +1123,9 @@ def add_attributed_to(object) do
Map.put(object, "attributedTo", attributed_to) Map.put(object, "attributedTo", attributed_to)
end end
# TODO: Revisit this
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
def prepare_attachments(object) do def prepare_attachments(object) do
attachments = attachments =
object object

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Maps
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo 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. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) 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 with {:ok, object} <- Object.create(object_data) do
map = Map.put(map, "object", object.data["id"]) map = Map.put(map, "object", object.data["id"])
@ -307,7 +308,7 @@ def make_like_data(
"cc" => cc, "cc" => cc,
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_emoji_reaction_data(user, object, emoji, activity_id) do def make_emoji_reaction_data(user, object, emoji, activity_id) do
@ -477,7 +478,7 @@ def make_follow_data(
"object" => followed_id, "object" => followed_id,
"state" => "pending" "state" => "pending"
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@ -546,7 +547,7 @@ def make_announce_data(
"cc" => [], "cc" => [],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_announce_data( def make_announce_data(
@ -563,7 +564,7 @@ def make_announce_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"] "context" => object.data["context"]
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_undo_data( def make_undo_data(
@ -582,7 +583,7 @@ def make_undo_data(
"cc" => [Pleroma.Constants.as_public()], "cc" => [Pleroma.Constants.as_public()],
"context" => context "context" => context
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
@spec add_announce_to_object(Activity.t(), Object.t()) :: @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], "to" => [followed.ap_id],
"object" => follow_activity.data "object" => follow_activity.data
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
#### Block-related helpers #### Block-related helpers
@ -650,7 +651,7 @@ def make_block_data(blocker, blocked, activity_id) do
"to" => [blocked.ap_id], "to" => [blocked.ap_id],
"object" => blocked.ap_id "object" => blocked.ap_id
} }
|> maybe_put("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
#### Create-related helpers #### Create-related helpers
@ -740,13 +741,12 @@ defp build_flag_object(_), do: []
def get_reports(params, page, page_size) do def get_reports(params, page, page_size) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put(:type, "Flag")
|> Map.put("type", "Flag") |> Map.put(:skip_preload, true)
|> Map.put("skip_preload", true) |> Map.put(:preload_report_notes, true)
|> Map.put("preload_report_notes", true) |> Map.put(:total, true)
|> Map.put("total", true) |> Map.put(:limit, page_size)
|> Map.put("limit", page_size) |> Map.put(:offset, (page - 1) * page_size)
|> Map.put("offset", (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset) ActivityPub.fetch_activities([], params, :offset)
end end
@ -871,7 +871,4 @@ def get_existing_votes(actor, %{data: %{"id" => id}}) do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data)) |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all() |> Repo.all()
end end
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
end end

View file

@ -213,34 +213,24 @@ def render("activity_collection.json", %{iri: iri}) do
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
def render("activity_collection_page.json", %{activities: activities, iri: iri}) do def render("activity_collection_page.json", %{
# this is sorted chronologically, so first activity is the newest (max) activities: activities,
{max_id, min_id, collection} = iri: iri,
if length(activities) > 0 do pagination: pagination
{ }) do
Enum.at(activities, 0).id, collection =
Enum.at(Enum.reverse(activities), 0).id, Enum.map(activities, fn activity ->
Enum.map(activities, fn act -> {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data data
end) end)
}
else
{
0,
0,
[]
}
end
%{ %{
"id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage", "type" => "OrderedCollectionPage",
"partOf" => iri, "partOf" => iri,
"orderedItems" => collection, "orderedItems" => collection
"next" => "#{iri}?max_id=#{min_id}&page=true"
} }
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
|> Map.merge(pagination)
end end
defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, false, _total), do: map

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -17,10 +16,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
@ -28,7 +25,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
require Logger require Logger
@descriptions Pleroma.Docs.JSON.compile()
@users_page_size 50 @users_page_size 50
plug( plug(
@ -62,7 +58,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true} %{scopes: ["write:follows"], admin: true}
when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] when action in [:user_follow, :user_unfollow]
) )
plug( plug(
@ -75,11 +71,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"], admin: true} %{scopes: ["read"], admin: true}
when action in [ when action in [
:config_show,
:list_log, :list_log,
:stats, :stats,
:relay_list,
:config_descriptions,
:need_reboot :need_reboot
] ]
) )
@ -89,7 +82,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
%{scopes: ["write"], admin: true} %{scopes: ["write"], admin: true}
when action in [ when action in [
:restart, :restart,
:config_update,
:resend_confirmation_email, :resend_confirmation_email,
:confirm_email, :confirm_email,
:reload_emoji :reload_emoji
@ -234,10 +226,10 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
activities = activities =
ActivityPub.fetch_statuses(nil, %{ ActivityPub.fetch_statuses(nil, %{
"instance" => instance, instance: instance,
"limit" => page_size, limit: page_size,
"offset" => (page - 1) * page_size, offset: (page - 1) * page_size,
"exclude_reblogs" => !with_reblogs && "true" exclude_reblogs: not with_reblogs
}) })
conn conn
@ -254,9 +246,9 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
activities = activities =
ActivityPub.fetch_user_activities(user, nil, %{ ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size, limit: page_size,
"godmode" => godmode, godmode: godmode,
"exclude_reblogs" => !with_reblogs && "true" exclude_reblogs: not with_reblogs
}) })
conn 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.") render_error(conn, :forbidden, "You can't revoke your own admin status.")
end 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" @doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname) (%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}) |> render("index.json", %{log: log})
end 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 def restart(conn, _params) do
with :ok <- configurable_from_database() do with :ok <- configurable_from_database() do
Restarter.Pleroma.restart(Config.get(:env), 50) Restarter.Pleroma.restart(Config.get(:env), 50)
@ -764,28 +613,6 @@ defp configurable_from_database do
end end
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 def reload_emoji(conn, _params) do
Pleroma.Emoji.reload() Pleroma.Emoji.reload()

View file

@ -0,0 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -42,12 +42,7 @@ def index(conn, params) do
end end
def create(%{body_params: params} = conn, _) do def create(%{body_params: params} = conn, _) do
params = params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
if params[:name] do
Map.put(params, :client_name, params[:name])
else
params
end
case App.create(params) do case App.create(params) do
{:ok, app} -> {:ok, app} ->
@ -59,12 +54,7 @@ def create(%{body_params: params} = conn, _) do
end end
def update(%{body_params: params} = conn, %{id: id}) do def update(%{body_params: params} = conn, %{id: id}) do
params = params = Pleroma.Maps.put_if_present(params, :client_name, params[:name])
if params[:name] do
Map.put(params, :client_name, params.name)
else
params
end
with {:ok, app} <- App.update(id, params) do with {:ok, app} <- App.update(id, params) do
render(conn, "show.json", app: app, admin: true) render(conn, "show.json", app: app, admin: true)

View file

@ -0,0 +1,67 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
def index(%{assigns: %{user: _admin}} = conn, params) do def index(%{assigns: %{user: _admin}} = conn, params) do
activities = activities =
ActivityPub.fetch_statuses(nil, %{ ActivityPub.fetch_statuses(nil, %{
"godmode" => params.godmode, godmode: params.godmode,
"local_only" => params.local_only, local_only: params.local_only,
"limit" => params.page_size, limit: params.page_size,
"offset" => (params.page - 1) * params.page_size, offset: (params.page - 1) * params.page_size,
"exclude_reblogs" => not params.with_reblogs exclude_reblogs: not params.with_reblogs
}) })
render(conn, "index.json", activities: activities, as: :activity) render(conn, "index.json", activities: activities, as: :activity)

View file

@ -76,7 +76,8 @@ def render("show.json", %{user: user}) do
"local" => user.local, "local" => user.local,
"roles" => User.roles(user), "roles" => User.roles(user),
"tags" => user.tags || [], "tags" => user.tags || [],
"confirmation_pending" => user.confirmation_pending "confirmation_pending" => user.confirmation_pending,
"url" => user.uri || user.ap_id
} }
end end

View file

@ -18,7 +18,7 @@ def render("index.json", %{reports: reports}) do
%{ %{
reports: reports:
reports[:items] reports[:items]
|> Enum.map(&Report.extract_report_info(&1)) |> Enum.map(&Report.extract_report_info/1)
|> Enum.map(&render(__MODULE__, "show.json", &1)) |> Enum.map(&render(__MODULE__, "show.json", &1))
|> Enum.reverse(), |> Enum.reverse(),
total: reports[:total] total: reports[:total]

View file

@ -0,0 +1,142 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,83 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,355 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -185,6 +185,7 @@ defp notification_type do
"mention", "mention",
"poll", "poll",
"pleroma:emoji_reaction", "pleroma:emoji_reaction",
"pleroma:chat_mention",
"move", "move",
"follow_request" "follow_request"
], ],

View file

@ -141,6 +141,11 @@ defp create_request do
allOf: [BooleanLike], allOf: [BooleanLike],
nullable: true, nullable: true,
description: "Receive poll notifications?" description: "Receive poll notifications?"
},
"pleroma:chat_mention": %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
} }
} }
} }

View file

@ -0,0 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -197,6 +197,13 @@ defp preview?(draft) do
defp changes(draft) do defp changes(draft) do
direct? = draft.visibility == "direct" 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 = changes =
%{ %{
@ -204,7 +211,7 @@ defp changes(draft) do
actor: draft.user, actor: draft.user,
context: draft.context, context: draft.context,
object: draft.object, object: draft.object,
additional: %{"cc" => draft.cc, "directMessage" => direct?} additional: additional
} }
|> Utils.maybe_add_list_data(draft.user, draft.visibility) |> Utils.maybe_add_list_data(draft.user, draft.visibility)

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger 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/, "<br>"), 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 def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block), {:ok, unblock_data, _} <- Builder.undo(blocker, block),
@ -73,6 +121,7 @@ def accept_follow_request(follower, followed) do
object: follow_activity.data["id"], object: follow_activity.data["id"],
type: "Accept" type: "Accept"
}) do }) do
Notification.update_notification_type(followed, follow_activity)
{:ok, follower} {:ok, follower}
end end
end end
@ -374,20 +423,10 @@ def listen(user, data) do
def post(user, %{status: _} = data) do def post(user, %{status: _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes ActivityPub.create(draft.changes, draft.preview?)
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end end
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 def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{ with %Activity{
actor: ^user_ap_id, actor: ^user_ap_id,
@ -427,12 +466,13 @@ def remove_mute(user, activity) do
{:ok, activity} {:ok, activity}
end end
def thread_muted?(%{id: nil} = _user, _activity), do: false def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
when is_binary("context") do
def thread_muted?(user, activity) do ThreadMute.exists?(user_id, context)
ThreadMute.exists?(user.id, activity.data["context"])
end end
def thread_muted?(_, _), do: false
def report(user, data) do def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id), with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),

View file

@ -429,7 +429,7 @@ def maybe_notify_mentioned_recipients(
%Activity{data: %{"to" => _to, "type" => type} = data} = activity %Activity{data: %{"to" => _to, "type" => type} = data} = activity
) )
when type == "Create" do when type == "Create" do
object = Object.normalize(activity) object = Object.normalize(activity, false)
object_data = object_data =
cond do cond do

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ControllerHelper do defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Pagination
# As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html # 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"] @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 do: conn
def add_link_headers(conn, activities, extra_params) do def add_link_headers(conn, activities, extra_params) do
case List.last(activities) do case get_pagination_fields(conn, activities, extra_params) do
%{id: max_id} -> %{"next" => next_url, "prev" => prev_url} ->
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}))
put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") 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
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 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) do
case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
%Pleroma.User{} = account -> assign(conn, :account, account) %Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end end
@ -99,11 +110,6 @@ def try_render(conn, _, _) do
render_error(conn, :not_implemented, "Can't display this activity") render_error(conn, :not_implemented, "Can't display this activity")
end 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 """ @doc """
Returns true if request specifies to include embedded relationships in account objects. 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 May only be used in selected account-related endpoints; has no effect for status- or

View file

@ -9,14 +9,12 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
def feed(conn, %{"tag" => raw_tag} = params) do def feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag) {format, tag} = parse_tag(raw_tag)
activities = activities =
%{"type" => ["Create"], "tag" => tag} %{type: ["Create"], tag: tag}
|> put_if_exist("max_id", params["max_id"]) |> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn

View file

@ -11,8 +11,6 @@ defmodule Pleroma.Web.Feed.UserController do
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_if_exist: 3]
plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect]) plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
action_fallback(:errors) 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 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities = activities =
%{ %{
"type" => ["Create"], type: ["Create"],
"actor_id" => user.ap_id 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() |> ActivityPub.fetch_public_or_unlisted_activities()
conn conn

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
json_response: 3 json_response: 3
] ]
alias Pleroma.Maps
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -160,23 +161,22 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
:discoverable :discoverable
] ]
|> Enum.reduce(%{}, fn key, acc -> |> 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) end)
|> add_if_present(params, :display_name, :name) |> Maps.put_if_present(:name, params[:display_name])
|> add_if_present(params, :note, :bio) |> Maps.put_if_present(:bio, params[:note])
|> add_if_present(params, :avatar, :avatar) |> Maps.put_if_present(:avatar, params[:avatar])
|> add_if_present(params, :header, :banner) |> Maps.put_if_present(:banner, params[:header])
|> add_if_present(params, :pleroma_background_image, :background) |> Maps.put_if_present(:background, params[:pleroma_background_image])
|> add_if_present( |> Maps.put_if_present(
params,
:fields_attributes,
:raw_fields, :raw_fields,
params[:fields_attributes],
&{:ok, normalize_fields_attributes(&1)} &{:ok, normalize_fields_attributes(&1)}
) )
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
|> add_if_present(params, :default_scope, :default_scope) |> Maps.put_if_present(:default_scope, params[:default_scope])
|> add_if_present(params["source"], "privacy", :default_scope) |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
|> add_if_present(params, :actor_type, :actor_type) |> Maps.put_if_present(:actor_type, params[:actor_type])
changeset = User.update_changeset(user, user_params) changeset = User.update_changeset(user, user_params)
@ -206,16 +206,6 @@ defp build_update_activity_params(user) do
} }
end 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 defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end) Enum.map(fields, fn {_, v} -> v end)
@ -254,9 +244,7 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
params = params =
params params
|> Map.delete(:tagged) |> Map.delete(:tagged)
|> Enum.filter(&(not is_nil(&1))) |> Map.put(:tag, params[:tagged])
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("tag", params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params) activities = ActivityPub.fetch_user_activities(user, reading_user, params)

View file

@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
@doc "GET /api/v1/conversations" @doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do def index(%{assigns: %{user: user}} = conn, params) do
params = stringify_pagination_params(params)
participations = Participation.for_user_with_last_activity_id(user, params) participations = Participation.for_user_with_last_activity_id(user, params)
conn 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) render(conn, "participation.json", participation: participation, for: user)
end end
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 end

View file

@ -42,8 +42,20 @@ def index(conn, %{account_id: account_id} = params) do
end end
end end
@default_notification_types ~w{
mention
follow
follow_request
reblog
favourite
move
pleroma:emoji_reaction
}
def index(%{assigns: %{user: user}} = conn, params) do 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) notifications = MastodonAPI.get_notifications(user, params)
conn conn

View file

@ -124,6 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do
defp prepare_tags(query, add_joined_tag \\ true) do defp prepare_tags(query, add_joined_tag \\ true) do
tags = tags =
query query
|> preprocess_uri_query()
|> String.split(~r/[^#\w]+/u, trim: true) |> String.split(~r/[^#\w]+/u, trim: true)
|> Enum.uniq_by(&String.downcase/1) |> Enum.uniq_by(&String.downcase/1)
@ -147,6 +148,20 @@ defp prepare_tags(query, add_joined_tag \\ true) do
end end
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 defp joined_tag(tags) do
tags tags
|> Enum.map(fn tag -> String.capitalize(tag) end) |> Enum.map(fn tag -> String.capitalize(tag) end)

View file

@ -359,9 +359,9 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do with %Activity{} = activity <- Activity.get_by_id(id) do
activities = activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{ ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user, blocking_user: user,
"user" => user, user: user,
"exclude_id" => activity.id exclude_id: activity.id
}) })
render(conn, "context.json", activity: activity, activities: activities, user: user) 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" @doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do 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) activities = ActivityPub.fetch_favourites(user, params)
conn conn

View file

@ -44,17 +44,15 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
def home(%{assigns: %{user: user}} = conn, params) do def home(%{assigns: %{user: user}} = conn, params) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put(:type, ["Create", "Announce"])
|> Map.put("type", ["Create", "Announce"]) |> Map.put(:blocking_user, user)
|> Map.put("blocking_user", user) |> Map.put(:muting_user, user)
|> Map.put("muting_user", user) |> Map.put(:reply_filtering_user, user)
|> Map.put("reply_filtering_user", user) |> Map.put(:announce_filtering_user, user)
|> Map.put("user", user) |> Map.put(:user, user)
recipients = [user.ap_id | User.following(user)]
activities = activities =
recipients [user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params) |> ActivityPub.fetch_activities(params)
|> Enum.reverse() |> Enum.reverse()
@ -71,10 +69,9 @@ def home(%{assigns: %{user: user}} = conn, params) do
def direct(%{assigns: %{user: user}} = conn, params) do def direct(%{assigns: %{user: user}} = conn, params) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put(:type, "Create")
|> Map.put("type", "Create") |> Map.put(:blocking_user, user)
|> Map.put("blocking_user", user) |> Map.put(:user, user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct") |> Map.put(:visibility, "direct")
activities = activities =
@ -93,9 +90,7 @@ def direct(%{assigns: %{user: user}} = conn, params) do
# GET /api/v1/timelines/public # GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do 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 = cfg_key =
if local_only do if local_only do
@ -111,11 +106,11 @@ def public(%{assigns: %{user: user}} = conn, params) do
else else
activities = activities =
params params
|> Map.put("type", ["Create"]) |> Map.put(:type, ["Create"])
|> Map.put("local_only", local_only) |> Map.put(:local_only, local_only)
|> Map.put("blocking_user", user) |> Map.put(:blocking_user, user)
|> Map.put("muting_user", user) |> Map.put(:muting_user, user)
|> Map.put("reply_filtering_user", user) |> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn
@ -130,39 +125,38 @@ def public(%{assigns: %{user: user}} = conn, params) do
defp hashtag_fetching(params, user, local_only) do defp hashtag_fetching(params, user, local_only) do
tags = tags =
[params["tag"], params["any"]] [params[:tag], params[:any]]
|> List.flatten() |> List.flatten()
|> Enum.uniq() |> Enum.uniq()
|> Enum.filter(& &1) |> Enum.reject(&is_nil/1)
|> Enum.map(&String.downcase(&1)) |> Enum.map(&String.downcase/1)
tag_all = tag_all =
params params
|> Map.get("all", []) |> Map.get(:all, [])
|> Enum.map(&String.downcase(&1)) |> Enum.map(&String.downcase/1)
tag_reject = tag_reject =
params params
|> Map.get("none", []) |> Map.get(:none, [])
|> Enum.map(&String.downcase(&1)) |> Enum.map(&String.downcase/1)
_activities = _activities =
params params
|> Map.put("type", "Create") |> Map.put(:type, "Create")
|> Map.put("local_only", local_only) |> Map.put(:local_only, local_only)
|> Map.put("blocking_user", user) |> Map.put(:blocking_user, user)
|> Map.put("muting_user", user) |> Map.put(:muting_user, user)
|> Map.put("user", user) |> Map.put(:user, user)
|> Map.put("tag", tags) |> Map.put(:tag, tags)
|> Map.put("tag_all", tag_all) |> Map.put(:tag_all, tag_all)
|> Map.put("tag_reject", tag_reject) |> Map.put(:tag_reject, tag_reject)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
end end
# GET /api/v1/timelines/tag/:tag # GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do 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) activities = hashtag_fetching(params, user, local_only)
conn conn

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
@ -82,15 +81,11 @@ defp cast_params(params) do
end end
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
ap_types = convert_and_filter_mastodon_types(mastodon_types) where(query, [n], n.type in ^mastodon_types)
where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
ap_types = convert_and_filter_mastodon_types(mastodon_types) where(query, [n], n.type not in ^mastodon_types)
where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
end end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do 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 end
defp restrict(query, _, _), do: query 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 end

View file

@ -235,6 +235,7 @@ defp do_render("show.json", %{user: user} = opts) do
# Pleroma extension # Pleroma extension
pleroma: %{ pleroma: %{
ap_id: user.ap_id,
confirmation_pending: user.confirmation_pending, confirmation_pending: user.confirmation_pending,
tags: user.tags, tags: user.tags,
hide_followers_count: user.hide_followers_count, hide_followers_count: user.hide_followers_count,

View file

@ -45,10 +45,6 @@ def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
defp with_vapid_key(data) do defp with_vapid_key(data) do
vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key] vapid_key = Application.get_env(:web_push_encryption, :vapid_details, [])[:public_key]
if vapid_key do Pleroma.Maps.put_if_present(data, "vapid_key", vapid_key)
Map.put(data, "vapid_key", vapid_key)
else
data
end
end end
end end

View file

@ -24,8 +24,8 @@ def render("participation.json", %{participation: participation, for: user}) do
last_activity_id = last_activity_id =
with nil <- participation.last_activity_id do with nil <- participation.last_activity_id do
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user, user: user,
"blocking_user" => user blocking_user: user
}) })
end end

View file

@ -69,7 +69,8 @@ def features do
if Config.get([:instance, :safe_dm_mentions]) do if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions" "safe_dm_mentions"
end, end,
"pleroma_emoji_reactions" "pleroma_emoji_reactions",
"pleroma_chat_messages"
] ]
|> Enum.filter(& &1) |> Enum.filter(& &1)
end end

View file

@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView 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 def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
activities = Enum.map(notifications, & &1.activity) activities = Enum.map(notifications, & &1.activity)
parent_activities = parent_activities =
activities activities
|> Enum.filter( |> Enum.filter(fn
&(Activity.mastodon_notification_type(&1) in [ %{data: %{"type" => type}} ->
"favourite", type in @parent_types
"reblog", end)
"pleroma:emoji_reaction"
])
)
|> Enum.map(& &1.data["object"]) |> Enum.map(& &1.data["object"])
|> Activity.create_by_object_ap_id() |> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left) |> Activity.with_preloaded_object(:left)
@ -42,7 +44,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
true -> true ->
move_activities_targets = move_activities_targets =
activities 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"])) |> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
actors = actors =
@ -79,8 +81,6 @@ def render(
end end
end end
mastodon_type = Activity.mastodon_notification_type(activity)
# Note: :relationships contain user mutes (needed for :muted flag in :status) # Note: :relationships contain user mutes (needed for :muted flag in :status)
status_render_opts = %{relationships: opts[:relationships]} status_render_opts = %{relationships: opts[:relationships]}
@ -91,7 +91,7 @@ def render(
) do ) do
response = %{ response = %{
id: to_string(notification.id), id: to_string(notification.id),
type: mastodon_type, type: notification.type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: account, account: account,
pleroma: %{ pleroma: %{
@ -99,7 +99,7 @@ def render(
} }
} }
case mastodon_type do case notification.type do
"mention" -> "mention" ->
put_status(response, activity, reading_user, status_render_opts) 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_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity) |> put_emoji(activity)
"pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts)
type when type in ["follow", "follow_request"] -> type when type in ["follow", "follow_request"] ->
response response
@ -132,6 +135,17 @@ defp put_emoji(response, activity) do
Map.put(response, :emoji, activity.data["content"]) Map.put(response, :emoji, activity.data["content"])
end 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 defp put_status(response, activity, reading_user, opts) do
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user}) status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
status_render = StatusView.render("show.json", status_render_opts) status_render = StatusView.render("show.json", status_render_opts)

View file

@ -30,7 +30,7 @@ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attac
defp with_media_attachments(data, _), do: data defp with_media_attachments(data, _), do: data
defp status_params(params) do defp status_params(params) do
data = %{ %{
text: params["status"], text: params["status"],
sensitive: params["sensitive"], sensitive: params["sensitive"],
spoiler_text: params["spoiler_text"], spoiler_text: params["spoiler_text"],
@ -39,10 +39,6 @@ defp status_params(params) do
poll: params["poll"], poll: params["poll"],
in_reply_to_id: params["in_reply_to_id"] in_reply_to_id: params["in_reply_to_id"]
} }
|> Pleroma.Maps.put_if_present(:media_ids, params["media_ids"])
case params["media_ids"] do
nil -> data
media_ids -> Map.put(data, :media_ids, media_ids)
end
end end
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA alias Pleroma.MFA
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.Registration alias Pleroma.Registration
@ -108,7 +109,7 @@ defp handle_existing_authorization(
if redirect_uri in String.split(app.redirect_uris) do if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri) redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token} 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) url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url) redirect(conn, external: url)
else else
@ -147,7 +148,7 @@ def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
if redirect_uri in String.split(app.redirect_uris) do if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri) redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token} 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) url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url) redirect(conn, external: url)
else else

View file

@ -126,10 +126,9 @@ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params)
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put(:type, "Create")
|> Map.put("type", "Create") |> Map.put(:favorited_by, user.ap_id)
|> Map.put("favorited_by", user.ap_id) |> Map.put(:blocking_user, for_user)
|> Map.put("blocking_user", for_user)
recipients = recipients =
if for_user do if for_user do

View file

@ -0,0 +1,174 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -42,15 +42,14 @@ def statuses(
Participation.get(participation_id, preload: [:conversation]) do Participation.get(participation_id, preload: [:conversation]) do
params = params =
params params
|> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put(:blocking_user, user)
|> Map.put("blocking_user", user) |> Map.put(:muting_user, user)
|> Map.put("muting_user", user) |> Map.put(:user, user)
|> Map.put("user", user)
activities = activities =
participation.conversation.ap_id participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context_query(params) |> 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() |> Enum.reverse()
conn conn

View file

@ -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 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 with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
params = params = Map.put(params, :type, ["Listen"])
params
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("type", ["Listen"])
activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do
require Logger require Logger
import Ecto.Query import Ecto.Query
defdelegate mastodon_notification_type(activity), to: Activity
@types ["Create", "Follow", "Announce", "Like", "Move"] @types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions" @doc "Performs sending notifications for user subscriptions"
@ -31,10 +29,10 @@ def perform(
when activity_type in @types do when activity_type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"]) 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) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity) object = Object.normalize(activity, false)
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user) direct_conversation_id = Activity.direct_conversation_id(activity, user)
@ -116,7 +114,7 @@ def build_content(
end end
def build_content(notification, actor, object, mastodon_type) do 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), 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, 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( def format_body(
%{activity: %{data: %{"type" => "Create"}}}, %{activity: %{data: %{"type" => "Create"}}},
actor, actor,
@ -151,7 +156,7 @@ def format_body(
mastodon_type mastodon_type
) )
when type in ["Follow", "Like"] do 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 case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you" "follow" -> "@#{actor.nickname} has followed you"
@ -166,15 +171,14 @@ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_typ
"New Direct Message" "New Direct Message"
end end
def format_title(%{activity: activity}, mastodon_type) do def format_title(%{type: type}, mastodon_type) do
mastodon_type = mastodon_type || mastodon_notification_type(activity) case mastodon_type || type do
case mastodon_type do
"mention" -> "New Mention" "mention" -> "New Mention"
"follow" -> "New Follower" "follow" -> "New Follower"
"follow_request" -> "New Follow Request" "follow_request" -> "New Follow Request"
"reblog" -> "New Repeat" "reblog" -> "New Repeat"
"favourite" -> "New Favorite" "favourite" -> "New Favorite"
"pleroma:chat_mention" -> "New Chat Message"
type -> "New #{String.capitalize(type || "event")}" type -> "New #{String.capitalize(type || "event")}"
end end
end end

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps() timestamps()
end 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 defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types) alerts = Map.take(alerts, @supported_alert_types)

View file

@ -160,9 +160,9 @@ defmodule Pleroma.Web.Router do
:right_delete_multiple :right_delete_multiple
) )
get("/relay", AdminAPIController, :relay_list) get("/relay", RelayController, :index)
post("/relay", AdminAPIController, :relay_follow) post("/relay", RelayController, :follow)
delete("/relay", AdminAPIController, :relay_unfollow) delete("/relay", RelayController, :unfollow)
post("/users/invite_token", InviteController, :create) post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index) get("/users/invites", InviteController, :index)
@ -194,9 +194,9 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id", StatusController, :delete) delete("/statuses/:id", StatusController, :delete)
get("/statuses", StatusController, :index) get("/statuses", StatusController, :index)
get("/config", AdminAPIController, :config_show) get("/config", ConfigController, :show)
post("/config", AdminAPIController, :config_update) post("/config", ConfigController, :update)
get("/config/descriptions", AdminAPIController, :config_descriptions) get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot) get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart) get("/restart", AdminAPIController, :restart)
@ -306,6 +306,15 @@ defmodule Pleroma.Web.Router do
scope [] do scope [] do
pipe_through(:authenticated_api) 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/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show) get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read) post("/conversations/read", ConversationController, :mark_as_read)
@ -571,13 +580,6 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end 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 pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"]) plug(:accepts, ["activity+json", "json"])
end end
@ -602,6 +604,7 @@ defmodule Pleroma.Web.Router do
get("/api/ap/whoami", ActivityPubController, :whoami) get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox) get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media) post("/api/ap/upload_media", ActivityPubController, :upload_media)

View file

@ -111,8 +111,14 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
%User{} = user -> %User{} = user ->
meta = Metadata.build_tags(%{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 = timeline =
ActivityPub.fetch_user_activities(user, nil, Map.take(params, @page_keys)) user
|> ActivityPub.fetch_user_activities(nil, params)
|> Enum.map(&represent/1) |> Enum.map(&represent/1)
prev_page_id = prev_page_id =

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
require Logger require Logger
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"] @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." @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) :: @spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
@ -89,29 +90,15 @@ def remove_socket(topic) do
if should_env_send?(), do: Registry.unregister(@registry, topic) if should_env_send?(), do: Registry.unregister(@registry, topic)
end 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)
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 if should_env_send?() do
List.wrap(topics)
|> Enum.each(fn topic ->
List.wrap(items)
|> Enum.each(fn item ->
spawn(fn -> do_stream(topic, item) end) spawn(fn -> do_stream(topic, item) end)
end)
end)
end end
:ok :ok
@ -200,6 +187,19 @@ defp do_stream(topic, %Notification{} = item)
end) end)
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 defp do_stream("user", item) do
Logger.debug("Trying to push to users") Logger.debug("Trying to push to users")

View file

@ -51,6 +51,29 @@ def render("update.json", %Activity{} = activity) do
|> Jason.encode!() |> Jason.encode!()
end 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 def render("conversation.json", %Participation{} = participation) do
%{ %{
event: "conversation", event: "conversation",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more