Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
This commit is contained in:
commit
be48c1bafb
177 changed files with 6412 additions and 2105 deletions
|
@ -1,4 +1,4 @@
|
|||
image: elixir:1.8.1
|
||||
image: elixir:1.9.4
|
||||
|
||||
variables: &global_variables
|
||||
POSTGRES_DB: pleroma_test
|
||||
|
@ -170,8 +170,7 @@ stop_review_app:
|
|||
|
||||
amd64:
|
||||
stage: release
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0
|
||||
image: elixir:1.10.3
|
||||
only: &release-only
|
||||
- stable@pleroma/pleroma
|
||||
- develop@pleroma/pleroma
|
||||
|
@ -208,8 +207,7 @@ amd64-musl:
|
|||
stage: release
|
||||
artifacts: *release-artifacts
|
||||
only: *release-only
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0-alpine
|
||||
image: elixir:1.10.3-alpine
|
||||
cache: *release-cache
|
||||
variables: *release-variables
|
||||
before_script: &before-release-musl
|
||||
|
@ -225,8 +223,7 @@ arm:
|
|||
only: *release-only
|
||||
tags:
|
||||
- arm32
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0-arm
|
||||
image: elixir:1.10.3
|
||||
cache: *release-cache
|
||||
variables: *release-variables
|
||||
before_script: *before-release
|
||||
|
@ -238,8 +235,7 @@ arm-musl:
|
|||
only: *release-only
|
||||
tags:
|
||||
- arm32
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0-arm-alpine
|
||||
image: elixir:1.10.3-alpine
|
||||
cache: *release-cache
|
||||
variables: *release-variables
|
||||
before_script: *before-release-musl
|
||||
|
@ -251,8 +247,7 @@ arm64:
|
|||
only: *release-only
|
||||
tags:
|
||||
- arm
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0-arm64
|
||||
image: elixir:1.10.3
|
||||
cache: *release-cache
|
||||
variables: *release-variables
|
||||
before_script: *before-release
|
||||
|
@ -265,7 +260,7 @@ arm64-musl:
|
|||
tags:
|
||||
- arm
|
||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine
|
||||
image: elixir:1.10.3-alpine
|
||||
cache: *release-cache
|
||||
variables: *release-variables
|
||||
before_script: *before-release-musl
|
||||
|
|
|
@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
## [unreleased]
|
||||
|
||||
### Changed
|
||||
- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
|
||||
- In Conversations, return only direct messages as `last_status`
|
||||
- MFR policy to set global expiration for all local Create activities
|
||||
- OGP rich media parser merged with TwitterCard
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
- **Breaking:** Emoji API: changed methods and renamed routes.
|
||||
|
@ -15,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||
|
||||
### Added
|
||||
- Chats: Added support for federated chats. For details, see the docs.
|
||||
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
|
||||
- Instance: Add `background_image` to configuration and `/api/v1/instance`
|
||||
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||
|
@ -25,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Configuration: `filename_display_max_length` option to set filename truncate limit, if filename display enabled (0 = no limit).
|
||||
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
|
||||
- Mix task to create trusted OAuth App.
|
||||
- Mix task to reset MFA for user accounts
|
||||
- Notifications: Added `follow_request` notification type.
|
||||
- Added `:reject_deletes` group to SimplePolicy
|
||||
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
|
||||
|
@ -36,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Mastodon API: Add support for filtering replies in public and home timelines
|
||||
- Admin API: endpoints for create/update/delete OAuth Apps.
|
||||
- Admin API: endpoint for status view.
|
||||
- OTP: Add command to reload emoji packs
|
||||
</details>
|
||||
|
||||
### Fixed
|
||||
|
@ -45,6 +52,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Filtering of push notifications on activities from blocked domains
|
||||
- Resolving Peertube accounts with Webfinger
|
||||
- `blob:` urls not being allowed by connect-src CSP
|
||||
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
|
||||
|
||||
## [Unreleased (patch)]
|
||||
|
||||
|
|
|
@ -52,12 +52,12 @@ defp render_views(user) do
|
|||
|
||||
defp opts_for_home_timeline(user) do
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"count" => "20",
|
||||
"muting_user" => user,
|
||||
"type" => ["Create", "Announce"],
|
||||
"user" => user,
|
||||
"with_muted" => "true"
|
||||
blocking_user: user,
|
||||
count: "20",
|
||||
muting_user: user,
|
||||
type: ["Create", "Announce"],
|
||||
user: user,
|
||||
with_muted: true
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -70,17 +70,17 @@ defp fetch_home_timeline(user) do
|
|||
ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last()
|
||||
|
||||
second_page_last =
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", first_page_last.id))
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id))
|
||||
|> Enum.reverse()
|
||||
|> List.last()
|
||||
|
||||
third_page_last =
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", second_page_last.id))
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id))
|
||||
|> Enum.reverse()
|
||||
|> List.last()
|
||||
|
||||
forth_page_last =
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, "max_id", third_page_last.id))
|
||||
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id))
|
||||
|> Enum.reverse()
|
||||
|> List.last()
|
||||
|
||||
|
@ -90,19 +90,19 @@ defp fetch_home_timeline(user) do
|
|||
},
|
||||
inputs: %{
|
||||
"1 page" => opts,
|
||||
"2 page" => Map.put(opts, "max_id", first_page_last.id),
|
||||
"3 page" => Map.put(opts, "max_id", second_page_last.id),
|
||||
"4 page" => Map.put(opts, "max_id", third_page_last.id),
|
||||
"5 page" => Map.put(opts, "max_id", forth_page_last.id),
|
||||
"1 page only media" => Map.put(opts, "only_media", "true"),
|
||||
"2 page" => Map.put(opts, :max_id, first_page_last.id),
|
||||
"3 page" => Map.put(opts, :max_id, second_page_last.id),
|
||||
"4 page" => Map.put(opts, :max_id, third_page_last.id),
|
||||
"5 page" => Map.put(opts, :max_id, forth_page_last.id),
|
||||
"1 page only media" => Map.put(opts, :only_media, true),
|
||||
"2 page only media" =>
|
||||
Map.put(opts, "max_id", first_page_last.id) |> Map.put("only_media", "true"),
|
||||
Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true),
|
||||
"3 page only media" =>
|
||||
Map.put(opts, "max_id", second_page_last.id) |> Map.put("only_media", "true"),
|
||||
Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true),
|
||||
"4 page only media" =>
|
||||
Map.put(opts, "max_id", third_page_last.id) |> Map.put("only_media", "true"),
|
||||
Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true),
|
||||
"5 page only media" =>
|
||||
Map.put(opts, "max_id", forth_page_last.id) |> Map.put("only_media", "true")
|
||||
Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true)
|
||||
},
|
||||
formatters: formatters()
|
||||
)
|
||||
|
@ -110,12 +110,12 @@ defp fetch_home_timeline(user) do
|
|||
|
||||
defp opts_for_direct_timeline(user) do
|
||||
%{
|
||||
:visibility => "direct",
|
||||
"blocking_user" => user,
|
||||
"count" => "20",
|
||||
"type" => "Create",
|
||||
"user" => user,
|
||||
"with_muted" => "true"
|
||||
visibility: "direct",
|
||||
blocking_user: user,
|
||||
count: "20",
|
||||
type: "Create",
|
||||
user: user,
|
||||
with_muted: true
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -130,7 +130,7 @@ defp fetch_direct_timeline(user) do
|
|||
|> Pagination.fetch_paginated(opts)
|
||||
|> List.last()
|
||||
|
||||
opts2 = Map.put(opts, "max_id", first_page_last.id)
|
||||
opts2 = Map.put(opts, :max_id, first_page_last.id)
|
||||
|
||||
second_page_last =
|
||||
recipients
|
||||
|
@ -138,7 +138,7 @@ defp fetch_direct_timeline(user) do
|
|||
|> Pagination.fetch_paginated(opts2)
|
||||
|> List.last()
|
||||
|
||||
opts3 = Map.put(opts, "max_id", second_page_last.id)
|
||||
opts3 = Map.put(opts, :max_id, second_page_last.id)
|
||||
|
||||
third_page_last =
|
||||
recipients
|
||||
|
@ -146,7 +146,7 @@ defp fetch_direct_timeline(user) do
|
|||
|> Pagination.fetch_paginated(opts3)
|
||||
|> List.last()
|
||||
|
||||
opts4 = Map.put(opts, "max_id", third_page_last.id)
|
||||
opts4 = Map.put(opts, :max_id, third_page_last.id)
|
||||
|
||||
forth_page_last =
|
||||
recipients
|
||||
|
@ -165,7 +165,7 @@ defp fetch_direct_timeline(user) do
|
|||
"2 page" => opts2,
|
||||
"3 page" => opts3,
|
||||
"4 page" => opts4,
|
||||
"5 page" => Map.put(opts4, "max_id", forth_page_last.id)
|
||||
"5 page" => Map.put(opts4, :max_id, forth_page_last.id)
|
||||
},
|
||||
formatters: formatters()
|
||||
)
|
||||
|
@ -173,34 +173,34 @@ defp fetch_direct_timeline(user) do
|
|||
|
||||
defp opts_for_public_timeline(user) do
|
||||
%{
|
||||
"type" => ["Create", "Announce"],
|
||||
"local_only" => false,
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
type: ["Create", "Announce"],
|
||||
local_only: false,
|
||||
blocking_user: user,
|
||||
muting_user: user
|
||||
}
|
||||
end
|
||||
|
||||
defp opts_for_public_timeline(user, :local) do
|
||||
%{
|
||||
"type" => ["Create", "Announce"],
|
||||
"local_only" => true,
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
type: ["Create", "Announce"],
|
||||
local_only: true,
|
||||
blocking_user: user,
|
||||
muting_user: user
|
||||
}
|
||||
end
|
||||
|
||||
defp opts_for_public_timeline(user, :tag) do
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"count" => "20",
|
||||
"local_only" => nil,
|
||||
"muting_user" => user,
|
||||
"tag" => ["tag"],
|
||||
"tag_all" => [],
|
||||
"tag_reject" => [],
|
||||
"type" => "Create",
|
||||
"user" => user,
|
||||
"with_muted" => "true"
|
||||
blocking_user: user,
|
||||
count: "20",
|
||||
local_only: nil,
|
||||
muting_user: user,
|
||||
tag: ["tag"],
|
||||
tag_all: [],
|
||||
tag_reject: [],
|
||||
type: "Create",
|
||||
user: user,
|
||||
with_muted: true
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -223,7 +223,7 @@ defp fetch_public_timeline(user, :tag) do
|
|||
end
|
||||
|
||||
defp fetch_public_timeline(user, :only_media) do
|
||||
opts = opts_for_public_timeline(user) |> Map.put("only_media", "true")
|
||||
opts = opts_for_public_timeline(user) |> Map.put(:only_media, true)
|
||||
|
||||
fetch_public_timeline(opts, "public timeline only media")
|
||||
end
|
||||
|
@ -245,15 +245,13 @@ defp fetch_public_timeline(user, :with_blocks) do
|
|||
|
||||
user = User.get_by_id(user.id)
|
||||
|
||||
opts = Map.put(opts, "blocking_user", user)
|
||||
opts = Map.put(opts, :blocking_user, user)
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
Benchee.run(%{
|
||||
"public timeline with user block" => fn ->
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
end
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
domains =
|
||||
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
|
||||
|
@ -269,30 +267,28 @@ defp fetch_public_timeline(user, :with_blocks) do
|
|||
end)
|
||||
|
||||
user = User.get_by_id(user.id)
|
||||
opts = Map.put(opts, "blocking_user", user)
|
||||
opts = Map.put(opts, :blocking_user, user)
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
"public timeline with domain block" => fn opts ->
|
||||
Benchee.run(%{
|
||||
"public timeline with domain block" => fn ->
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
end
|
||||
}
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
defp fetch_public_timeline(opts, title) when is_binary(title) do
|
||||
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()
|
||||
|
||||
second_page_last =
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, "max_id", first_page_last.id))
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
third_page_last =
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, "max_id", second_page_last.id))
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
forth_page_last =
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, "max_id", third_page_last.id))
|
||||
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
Benchee.run(
|
||||
|
@ -303,17 +299,17 @@ defp fetch_public_timeline(opts, title) when is_binary(title) do
|
|||
},
|
||||
inputs: %{
|
||||
"1 page" => opts,
|
||||
"2 page" => Map.put(opts, "max_id", first_page_last.id),
|
||||
"3 page" => Map.put(opts, "max_id", second_page_last.id),
|
||||
"4 page" => Map.put(opts, "max_id", third_page_last.id),
|
||||
"5 page" => Map.put(opts, "max_id", forth_page_last.id)
|
||||
"2 page" => Map.put(opts, :max_id, first_page_last.id),
|
||||
"3 page" => Map.put(opts, :max_id, second_page_last.id),
|
||||
"4 page" => Map.put(opts, :max_id, third_page_last.id),
|
||||
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
|
||||
},
|
||||
formatters: formatters()
|
||||
)
|
||||
end
|
||||
|
||||
defp opts_for_notifications do
|
||||
%{"count" => "20", "with_muted" => "true"}
|
||||
%{count: "20", with_muted: true}
|
||||
end
|
||||
|
||||
defp fetch_notifications(user) do
|
||||
|
@ -322,15 +318,15 @@ defp fetch_notifications(user) do
|
|||
first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last()
|
||||
|
||||
second_page_last =
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, "max_id", first_page_last.id))
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
third_page_last =
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, "max_id", second_page_last.id))
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
forth_page_last =
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, "max_id", third_page_last.id))
|
||||
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id))
|
||||
|> List.last()
|
||||
|
||||
Benchee.run(
|
||||
|
@ -341,10 +337,10 @@ defp fetch_notifications(user) do
|
|||
},
|
||||
inputs: %{
|
||||
"1 page" => opts,
|
||||
"2 page" => Map.put(opts, "max_id", first_page_last.id),
|
||||
"3 page" => Map.put(opts, "max_id", second_page_last.id),
|
||||
"4 page" => Map.put(opts, "max_id", third_page_last.id),
|
||||
"5 page" => Map.put(opts, "max_id", forth_page_last.id)
|
||||
"2 page" => Map.put(opts, :max_id, first_page_last.id),
|
||||
"3 page" => Map.put(opts, :max_id, second_page_last.id),
|
||||
"4 page" => Map.put(opts, :max_id, third_page_last.id),
|
||||
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
|
||||
},
|
||||
formatters: formatters()
|
||||
)
|
||||
|
@ -354,13 +350,13 @@ defp fetch_favourites(user) do
|
|||
first_page_last = ActivityPub.fetch_favourites(user) |> List.last()
|
||||
|
||||
second_page_last =
|
||||
ActivityPub.fetch_favourites(user, %{"max_id" => first_page_last.id}) |> List.last()
|
||||
ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last()
|
||||
|
||||
third_page_last =
|
||||
ActivityPub.fetch_favourites(user, %{"max_id" => second_page_last.id}) |> List.last()
|
||||
ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last()
|
||||
|
||||
forth_page_last =
|
||||
ActivityPub.fetch_favourites(user, %{"max_id" => third_page_last.id}) |> List.last()
|
||||
ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last()
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
|
@ -370,10 +366,10 @@ defp fetch_favourites(user) do
|
|||
},
|
||||
inputs: %{
|
||||
"1 page" => %{},
|
||||
"2 page" => %{"max_id" => first_page_last.id},
|
||||
"3 page" => %{"max_id" => second_page_last.id},
|
||||
"4 page" => %{"max_id" => third_page_last.id},
|
||||
"5 page" => %{"max_id" => forth_page_last.id}
|
||||
"2 page" => %{:max_id => first_page_last.id},
|
||||
"3 page" => %{:max_id => second_page_last.id},
|
||||
"4 page" => %{:max_id => third_page_last.id},
|
||||
"5 page" => %{:max_id => forth_page_last.id}
|
||||
},
|
||||
formatters: formatters()
|
||||
)
|
||||
|
@ -381,8 +377,8 @@ defp fetch_favourites(user) do
|
|||
|
||||
defp opts_for_long_thread(user) do
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"user" => user
|
||||
blocking_user: user,
|
||||
user: user
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -392,9 +388,9 @@ defp fetch_long_thread(user) do
|
|||
|
||||
opts = opts_for_long_thread(user)
|
||||
|
||||
private_input = {private.data["context"], Map.put(opts, "exclude_id", private.id)}
|
||||
private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)}
|
||||
|
||||
public_input = {public.data["context"], Map.put(opts, "exclude_id", public.id)}
|
||||
public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)}
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
|
@ -514,13 +510,13 @@ defp render_long_thread(user) do
|
|||
public_context =
|
||||
ActivityPub.fetch_activities_for_context(
|
||||
public.data["context"],
|
||||
Map.put(fetch_opts, "exclude_id", public.id)
|
||||
Map.put(fetch_opts, :exclude_id, public.id)
|
||||
)
|
||||
|
||||
private_context =
|
||||
ActivityPub.fetch_activities_for_context(
|
||||
private.data["context"],
|
||||
Map.put(fetch_opts, "exclude_id", private.id)
|
||||
Map.put(fetch_opts, :exclude_id, private.id)
|
||||
)
|
||||
|
||||
Benchee.run(
|
||||
|
@ -551,14 +547,14 @@ defp fetch_timelines_with_reply_filtering(user) do
|
|||
end,
|
||||
"Public timeline with reply filtering - following" => fn ->
|
||||
public_params
|
||||
|> Map.put("reply_visibility", "following")
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put(:reply_visibility, "following")
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
end,
|
||||
"Public timeline with reply filtering - self" => fn ->
|
||||
public_params
|
||||
|> Map.put("reply_visibility", "self")
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put(:reply_visibility, "self")
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
end
|
||||
},
|
||||
|
@ -577,16 +573,16 @@ defp fetch_timelines_with_reply_filtering(user) do
|
|||
"Home timeline with reply filtering - following" => fn ->
|
||||
private_params =
|
||||
private_params
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put("reply_visibility", "following")
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> Map.put(:reply_visibility, "following")
|
||||
|
||||
ActivityPub.fetch_activities(recipients, private_params)
|
||||
end,
|
||||
"Home timeline with reply filtering - self" => fn ->
|
||||
private_params =
|
||||
private_params
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put("reply_visibility", "self")
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> Map.put(:reply_visibility, "self")
|
||||
|
||||
ActivityPub.fetch_activities(recipients, private_params)
|
||||
end
|
||||
|
|
|
@ -100,14 +100,14 @@ defp hashtag_fetching(params, user, local_only) do
|
|||
|
||||
_activities =
|
||||
params
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("local_only", local_only)
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put("tag", tags)
|
||||
|> Map.put("tag_all", tag_all)
|
||||
|> Map.put("tag_reject", tag_reject)
|
||||
|> Map.put(:type, "Create")
|
||||
|> Map.put(:local_only, local_only)
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:tag, tags)
|
||||
|> Map.put(:tag_all, tag_all)
|
||||
|> Map.put(:tag_reject, tag_reject)
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -372,6 +372,8 @@
|
|||
|
||||
config :pleroma, :mrf_subchain, match_actor: %{}
|
||||
|
||||
config :pleroma, :mrf_activity_expiration, days: 365
|
||||
|
||||
config :pleroma, :mrf_vocabulary,
|
||||
accept: [],
|
||||
reject: []
|
||||
|
@ -386,7 +388,6 @@
|
|||
ignore_tld: ["local", "localdomain", "lan"],
|
||||
parsers: [
|
||||
Pleroma.Web.RichMedia.Parsers.TwitterCard,
|
||||
Pleroma.Web.RichMedia.Parsers.OGP,
|
||||
Pleroma.Web.RichMedia.Parsers.OEmbed
|
||||
],
|
||||
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
|
||||
|
|
|
@ -1471,6 +1471,21 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_activity_expiration,
|
||||
label: "MRF Activity Expiration Policy",
|
||||
type: :group,
|
||||
description: "Adds expiration to all local Create Note activities",
|
||||
children: [
|
||||
%{
|
||||
key: :days,
|
||||
type: :integer,
|
||||
description: "Default global expiration time for all local Create activities (in days)",
|
||||
suggestions: [90, 365]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_subchain,
|
||||
|
@ -1608,14 +1623,12 @@
|
|||
# %{
|
||||
# group: :pleroma,
|
||||
# key: :mrf_user_allowlist,
|
||||
# type: :group,
|
||||
# type: :map,
|
||||
# description:
|
||||
# "The keys in this section are the domain names that the policy should apply to." <>
|
||||
# " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
|
||||
# children: [
|
||||
# ["example.org": ["https://example.org/users/admin"]],
|
||||
# suggestions: [
|
||||
# ["example.org": ["https://example.org/users/admin"]]
|
||||
# %{"example.org" => ["https://example.org/users/admin"]}
|
||||
# ]
|
||||
# ]
|
||||
# },
|
||||
|
@ -2091,9 +2104,7 @@
|
|||
description:
|
||||
"List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
|
||||
suggestions: [
|
||||
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
|
||||
Pleroma.Web.RichMedia.Parsers.OEmbed,
|
||||
Pleroma.Web.RichMedia.Parsers.OGP,
|
||||
Pleroma.Web.RichMedia.Parsers.TwitterCard
|
||||
]
|
||||
},
|
||||
|
|
248
docs/API/chats.md
Normal file
248
docs/API/chats.md
Normal 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.
|
|
@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
|||
Has these additional fields under the `pleroma` object:
|
||||
|
||||
- `unread_count`: contains number unread notifications
|
||||
|
||||
## Streaming
|
||||
|
||||
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
|
||||
|
|
|
@ -44,3 +44,11 @@ Currently, only .zip archives are recognized as remote pack files and packs are
|
|||
The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
|
||||
|
||||
The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).
|
||||
|
||||
## Reload emoji packs
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji reload
|
||||
```
|
||||
|
||||
This command only works with OTP releases.
|
||||
|
|
|
@ -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
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user set <nickname> [option ...]
|
||||
|
|
35
docs/ap_extensions.md
Normal file
35
docs/ap_extensions.md
Normal 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`.
|
|
@ -39,7 +39,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
|
||||
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
|
||||
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
|
||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certain instances (See [`:mrf_simple`](#mrf_simple)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
|
||||
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
|
||||
|
@ -49,7 +49,8 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
|
||||
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
|
||||
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Adds expiration to all local Create activities (see [`:mrf_activity_expiration`](#mrf_activity_expiration)).
|
||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
|
||||
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
|
||||
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
|
||||
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
|
||||
|
@ -137,8 +138,9 @@ their ActivityPub ID.
|
|||
An example:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :mrf_user_allowlist,
|
||||
"example.org": ["https://example.org/users/admin"]
|
||||
config :pleroma, :mrf_user_allowlist, %{
|
||||
"example.org" => ["https://example.org/users/admin"]
|
||||
}
|
||||
```
|
||||
|
||||
#### :mrf_object_age
|
||||
|
@ -154,6 +156,10 @@ config :pleroma, :mrf_user_allowlist,
|
|||
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
||||
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
|
||||
|
||||
#### :mrf_activity_expiration
|
||||
|
||||
* `days`: Default global expiration time for all local Create activities (in days)
|
||||
|
||||
### :activitypub
|
||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
elixir_version=1.8.2
|
||||
erlang_version=21.3.7
|
||||
elixir_version=1.9.4
|
||||
erlang_version=22.3.4.1
|
||||
|
|
|
@ -37,18 +37,17 @@ server {
|
|||
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# Add TLSv1.0 to support older devices
|
||||
ssl_protocols TLSv1.2;
|
||||
# Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.)
|
||||
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_prefer_server_ciphers off;
|
||||
# In case of an old server with an OpenSSL version of 1.0.2 or below,
|
||||
# leave only prime256v1 or comment out the following line.
|
||||
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
|
||||
|
|
|
@ -72,8 +72,7 @@ defp create(group, settings) do
|
|||
group
|
||||
|> Pleroma.Config.Loader.filter_group(settings)
|
||||
|> Enum.each(fn {key, value} ->
|
||||
key = inspect(key)
|
||||
{:ok, _} = ConfigDB.update_or_create(%{group: inspect(group), key: key, value: value})
|
||||
{:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
|
||||
|
||||
shell_info("Settings for key #{key} migrated.")
|
||||
end)
|
||||
|
@ -131,12 +130,9 @@ defp write_and_delete(config, file, delete?) do
|
|||
end
|
||||
|
||||
defp write(config, file) do
|
||||
value =
|
||||
config.value
|
||||
|> ConfigDB.from_binary()
|
||||
|> inspect(limit: :infinity)
|
||||
value = inspect(config.value, limit: :infinity)
|
||||
|
||||
IO.write(file, "config #{config.group}, #{config.key}, #{value}\r\n\r\n")
|
||||
IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
|
||||
|
||||
config
|
||||
end
|
||||
|
|
|
@ -237,6 +237,12 @@ def run(["gen-pack" | args]) do
|
|||
end
|
||||
end
|
||||
|
||||
def run(["reload"]) do
|
||||
start_pleroma()
|
||||
Pleroma.Emoji.reload()
|
||||
IO.puts("Emoji packs have been reloaded.")
|
||||
end
|
||||
|
||||
defp fetch_and_decode(from) do
|
||||
with {:ok, json} <- fetch(from) do
|
||||
Jason.decode!(json)
|
||||
|
|
|
@ -144,6 +144,18 @@ def run(["reset_password", nickname]) do
|
|||
end
|
||||
end
|
||||
|
||||
def run(["reset_mfa", nickname]) do
|
||||
start_pleroma()
|
||||
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, _token} <- Pleroma.MFA.disable(user) do
|
||||
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("No local user #{nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def run(["deactivate", nickname]) do
|
||||
start_pleroma()
|
||||
|
||||
|
|
|
@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
|
|||
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
|
||||
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
|
||||
@mastodon_notification_types %{
|
||||
"Create" => "mention",
|
||||
"Follow" => ["follow", "follow_request"],
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite",
|
||||
"Move" => "move",
|
||||
"EmojiReact" => "pleroma:emoji_reaction"
|
||||
}
|
||||
|
||||
schema "activities" do
|
||||
field(:data, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
|
@ -41,6 +31,10 @@ defmodule Pleroma.Activity do
|
|||
field(:recipients, {:array, :string}, default: [])
|
||||
field(:thread_muted?, :boolean, virtual: true)
|
||||
|
||||
# A field that can be used if you need to join some kind of other
|
||||
# id to order / paginate this field by
|
||||
field(:pagination_id, :string, virtual: true)
|
||||
|
||||
# This is a fake relation,
|
||||
# do not use outside of with_preloaded_user_actor/with_joined_user_actor
|
||||
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
|
||||
|
@ -300,32 +294,6 @@ def follow_accepted?(
|
|||
|
||||
def follow_accepted?(_), do: false
|
||||
|
||||
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
||||
do: unquote(type)
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{}), do: nil
|
||||
|
||||
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
|
||||
@doc "Converts Mastodon notification type to AR activity type"
|
||||
def from_mastodon_notification_type(type) do
|
||||
with {k, _v} <-
|
||||
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
|
||||
k
|
||||
end
|
||||
end
|
||||
|
||||
def all_by_actor_and_id(actor, status_ids \\ [])
|
||||
def all_by_actor_and_id(_actor, []), do: []
|
||||
|
||||
|
|
|
@ -92,10 +92,10 @@ def handle_command(state, "home") do
|
|||
|
||||
params =
|
||||
%{}
|
||||
|> Map.put("type", ["Create"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put(:type, ["Create"])
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|
||||
activities =
|
||||
[user.ap_id | Pleroma.User.following(user)]
|
||||
|
|
72
lib/pleroma/chat.ex
Normal file
72
lib/pleroma/chat.ex
Normal 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
|
117
lib/pleroma/chat/message_reference.ex
Normal file
117
lib/pleroma/chat/message_reference.ex
Normal 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
|
|
@ -6,7 +6,7 @@ defmodule Pleroma.ConfigDB do
|
|||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Ecto.Query, only: [select: 3]
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
alias __MODULE__
|
||||
|
@ -14,16 +14,6 @@ defmodule Pleroma.ConfigDB do
|
|||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@full_key_update [
|
||||
{:pleroma, :ecto_repos},
|
||||
{:quack, :meta},
|
||||
{:mime, :types},
|
||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||
{:auto_linker, :opts},
|
||||
{:swarm, :node_blacklist},
|
||||
{:logger, :backends}
|
||||
]
|
||||
|
||||
@full_subkey_update [
|
||||
{:pleroma, :assets, :mascots},
|
||||
{:pleroma, :emoji, :groups},
|
||||
|
@ -32,14 +22,10 @@ defmodule Pleroma.ConfigDB do
|
|||
{:pleroma, :mrf_keyword, :replace}
|
||||
]
|
||||
|
||||
@regex ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
|
||||
|
||||
@delimiters ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
|
||||
|
||||
schema "config" do
|
||||
field(:key, :string)
|
||||
field(:group, :string)
|
||||
field(:value, :binary)
|
||||
field(:key, Pleroma.EctoType.Config.Atom)
|
||||
field(:group, Pleroma.EctoType.Config.Atom)
|
||||
field(:value, Pleroma.EctoType.Config.BinaryValue)
|
||||
field(:db, {:array, :string}, virtual: true, default: [])
|
||||
|
||||
timestamps()
|
||||
|
@ -51,10 +37,6 @@ def get_all_as_keyword do
|
|||
|> select([c], {c.group, c.key, c.value})
|
||||
|> Repo.all()
|
||||
|> Enum.reduce([], fn {group, key, value}, acc ->
|
||||
group = ConfigDB.from_string(group)
|
||||
key = ConfigDB.from_string(key)
|
||||
value = from_binary(value)
|
||||
|
||||
Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
|
||||
end)
|
||||
end
|
||||
|
@ -64,50 +46,41 @@ def get_by_params(params), do: Repo.get_by(ConfigDB, params)
|
|||
|
||||
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
|
||||
def changeset(config, params \\ %{}) do
|
||||
params = Map.put(params, :value, transform(params[:value]))
|
||||
|
||||
config
|
||||
|> cast(params, [:key, :group, :value])
|
||||
|> validate_required([:key, :group, :value])
|
||||
|> unique_constraint(:key, name: :config_group_key_index)
|
||||
end
|
||||
|
||||
@spec create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
def create(params) do
|
||||
defp create(params) do
|
||||
%ConfigDB{}
|
||||
|> changeset(params)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@spec update(ConfigDB.t(), map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
def update(%ConfigDB{} = config, %{value: value}) do
|
||||
defp update(%ConfigDB{} = config, %{value: value}) do
|
||||
config
|
||||
|> changeset(%{value: value})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@spec get_db_keys(ConfigDB.t()) :: [String.t()]
|
||||
def get_db_keys(%ConfigDB{} = config) do
|
||||
config.value
|
||||
|> ConfigDB.from_binary()
|
||||
|> get_db_keys(config.key)
|
||||
end
|
||||
|
||||
@spec get_db_keys(keyword(), any()) :: [String.t()]
|
||||
def get_db_keys(value, key) do
|
||||
keys =
|
||||
if Keyword.keyword?(value) do
|
||||
value |> Keyword.keys() |> Enum.map(&convert(&1))
|
||||
Keyword.keys(value)
|
||||
else
|
||||
[convert(key)]
|
||||
[key]
|
||||
end
|
||||
|
||||
Enum.map(keys, &to_json_types(&1))
|
||||
end
|
||||
|
||||
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
|
||||
def merge_group(group, key, old_value, new_value) do
|
||||
new_keys = to_map_set(new_value)
|
||||
new_keys = to_mapset(new_value)
|
||||
|
||||
intersect_keys =
|
||||
old_value |> to_map_set() |> MapSet.intersection(new_keys) |> MapSet.to_list()
|
||||
intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
|
||||
|
||||
merged_value = ConfigDB.merge(old_value, new_value)
|
||||
|
||||
|
@ -120,12 +93,10 @@ def merge_group(group, key, old_value, new_value) do
|
|||
[]
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.reduce(merged_value, fn subkey, acc ->
|
||||
Keyword.put(acc, subkey, new_value[subkey])
|
||||
end)
|
||||
|> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
|
||||
end
|
||||
|
||||
defp to_map_set(keyword) do
|
||||
defp to_mapset(keyword) do
|
||||
keyword
|
||||
|> Keyword.keys()
|
||||
|> MapSet.new()
|
||||
|
@ -159,43 +130,40 @@ defp deep_merge(_key, value1, value2) do
|
|||
|
||||
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
def update_or_create(params) do
|
||||
params = Map.put(params, :value, to_elixir_types(params[:value]))
|
||||
search_opts = Map.take(params, [:group, :key])
|
||||
|
||||
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
|
||||
{:partial_update, true, config} <-
|
||||
{:partial_update, can_be_partially_updated?(config), config},
|
||||
old_value <- from_binary(config.value),
|
||||
transformed_value <- do_transform(params[:value]),
|
||||
{:can_be_merged, true, config} <- {:can_be_merged, is_list(transformed_value), config},
|
||||
new_value <-
|
||||
merge_group(
|
||||
ConfigDB.from_string(config.group),
|
||||
ConfigDB.from_string(config.key),
|
||||
old_value,
|
||||
transformed_value
|
||||
) do
|
||||
ConfigDB.update(config, %{value: new_value})
|
||||
{_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
|
||||
{_, true, config} <-
|
||||
{:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
|
||||
new_value = merge_group(config.group, config.key, config.value, params[:value])
|
||||
update(config, %{value: new_value})
|
||||
else
|
||||
{reason, false, config} when reason in [:partial_update, :can_be_merged] ->
|
||||
ConfigDB.update(config, params)
|
||||
update(config, params)
|
||||
|
||||
nil ->
|
||||
ConfigDB.create(params)
|
||||
create(params)
|
||||
end
|
||||
end
|
||||
|
||||
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
|
||||
|
||||
defp only_full_update?(%ConfigDB{} = config) do
|
||||
config_group = ConfigDB.from_string(config.group)
|
||||
config_key = ConfigDB.from_string(config.key)
|
||||
defp only_full_update?(%ConfigDB{group: group, key: key}) do
|
||||
full_key_update = [
|
||||
{:pleroma, :ecto_repos},
|
||||
{:quack, :meta},
|
||||
{:mime, :types},
|
||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||
{:auto_linker, :opts},
|
||||
{:swarm, :node_blacklist},
|
||||
{:logger, :backends}
|
||||
]
|
||||
|
||||
Enum.any?(@full_key_update, fn
|
||||
{group, key} when is_list(key) ->
|
||||
config_group == group and config_key in key
|
||||
|
||||
{group, key} ->
|
||||
config_group == group and config_key == key
|
||||
Enum.any?(full_key_update, fn
|
||||
{s_group, s_key} ->
|
||||
group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -205,11 +173,10 @@ def delete(params) do
|
|||
|
||||
with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
|
||||
{config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
|
||||
old_value <- from_binary(config.value),
|
||||
keys <- Enum.map(sub_keys, &do_transform_string(&1)),
|
||||
{:partial_remove, config, new_value} when new_value != [] <-
|
||||
{:partial_remove, config, Keyword.drop(old_value, keys)} do
|
||||
ConfigDB.update(config, %{value: new_value})
|
||||
keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
|
||||
{_, config, new_value} when new_value != [] <-
|
||||
{:partial_remove, config, Keyword.drop(config.value, keys)} do
|
||||
update(config, %{value: new_value})
|
||||
else
|
||||
{:partial_remove, config, []} ->
|
||||
Repo.delete(config)
|
||||
|
@ -225,37 +192,32 @@ def delete(params) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec from_binary(binary()) :: term()
|
||||
def from_binary(binary), do: :erlang.binary_to_term(binary)
|
||||
|
||||
@spec from_binary_with_convert(binary()) :: any()
|
||||
def from_binary_with_convert(binary) do
|
||||
binary
|
||||
|> from_binary()
|
||||
|> do_convert()
|
||||
@spec to_json_types(term()) :: map() | list() | boolean() | String.t()
|
||||
def to_json_types(entity) when is_list(entity) do
|
||||
Enum.map(entity, &to_json_types/1)
|
||||
end
|
||||
|
||||
@spec from_string(String.t()) :: atom() | no_return()
|
||||
def from_string(string), do: do_transform_string(string)
|
||||
def to_json_types(%Regex{} = entity), do: inspect(entity)
|
||||
|
||||
@spec convert(any()) :: any()
|
||||
def convert(entity), do: do_convert(entity)
|
||||
|
||||
defp do_convert(entity) when is_list(entity) do
|
||||
for v <- entity, into: [], do: do_convert(v)
|
||||
def to_json_types(entity) when is_map(entity) do
|
||||
Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
|
||||
end
|
||||
|
||||
defp do_convert(%Regex{} = entity), do: inspect(entity)
|
||||
def to_json_types({:args, args}) when is_list(args) do
|
||||
arguments =
|
||||
Enum.map(args, fn
|
||||
arg when is_tuple(arg) -> inspect(arg)
|
||||
arg -> to_json_types(arg)
|
||||
end)
|
||||
|
||||
defp do_convert(entity) when is_map(entity) do
|
||||
for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)}
|
||||
%{"tuple" => [":args", arguments]}
|
||||
end
|
||||
|
||||
defp do_convert({:proxy_url, {type, :localhost, port}}) do
|
||||
%{"tuple" => [":proxy_url", %{"tuple" => [do_convert(type), "localhost", port]}]}
|
||||
def to_json_types({:proxy_url, {type, :localhost, port}}) do
|
||||
%{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
|
||||
end
|
||||
|
||||
defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
|
||||
def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
|
||||
ip =
|
||||
host
|
||||
|> :inet_parse.ntoa()
|
||||
|
@ -264,66 +226,64 @@ defp do_convert({:proxy_url, {type, host, port}}) when is_tuple(host) do
|
|||
%{
|
||||
"tuple" => [
|
||||
":proxy_url",
|
||||
%{"tuple" => [do_convert(type), ip, port]}
|
||||
%{"tuple" => [to_json_types(type), ip, port]}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp do_convert({:proxy_url, {type, host, port}}) do
|
||||
def to_json_types({:proxy_url, {type, host, port}}) do
|
||||
%{
|
||||
"tuple" => [
|
||||
":proxy_url",
|
||||
%{"tuple" => [do_convert(type), to_string(host), port]}
|
||||
%{"tuple" => [to_json_types(type), to_string(host), port]}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
|
||||
def to_json_types({:partial_chain, entity}),
|
||||
do: %{"tuple" => [":partial_chain", inspect(entity)]}
|
||||
|
||||
defp do_convert(entity) when is_tuple(entity) do
|
||||
def to_json_types(entity) when is_tuple(entity) do
|
||||
value =
|
||||
entity
|
||||
|> Tuple.to_list()
|
||||
|> do_convert()
|
||||
|> to_json_types()
|
||||
|
||||
%{"tuple" => value}
|
||||
end
|
||||
|
||||
defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
|
||||
def to_json_types(entity) when is_binary(entity), do: entity
|
||||
|
||||
def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
|
||||
entity
|
||||
end
|
||||
|
||||
defp do_convert(entity)
|
||||
when is_atom(entity) and entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
|
||||
def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
|
||||
":#{entity}"
|
||||
end
|
||||
|
||||
defp do_convert(entity) when is_atom(entity), do: inspect(entity)
|
||||
def to_json_types(entity) when is_atom(entity), do: inspect(entity)
|
||||
|
||||
defp do_convert(entity) when is_binary(entity), do: entity
|
||||
@spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
|
||||
def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
|
||||
arguments =
|
||||
Enum.map(args, fn arg ->
|
||||
if String.contains?(arg, ["{", "}"]) do
|
||||
{elem, []} = Code.eval_string(arg)
|
||||
elem
|
||||
else
|
||||
to_elixir_types(arg)
|
||||
end
|
||||
end)
|
||||
|
||||
@spec transform(any()) :: binary() | no_return()
|
||||
def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do
|
||||
entity
|
||||
|> do_transform()
|
||||
|> to_binary()
|
||||
{:args, arguments}
|
||||
end
|
||||
|
||||
def transform(entity), do: to_binary(entity)
|
||||
|
||||
@spec transform_with_out_binary(any()) :: any()
|
||||
def transform_with_out_binary(entity), do: do_transform(entity)
|
||||
|
||||
@spec to_binary(any()) :: binary()
|
||||
def to_binary(entity), do: :erlang.term_to_binary(entity)
|
||||
|
||||
defp do_transform(%Regex{} = entity), do: entity
|
||||
|
||||
defp do_transform(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
|
||||
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
|
||||
def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
|
||||
{:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
|
||||
end
|
||||
|
||||
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
|
||||
def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
|
||||
{partial_chain, []} =
|
||||
entity
|
||||
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
|
||||
|
@ -332,25 +292,51 @@ defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
|
|||
{:partial_chain, partial_chain}
|
||||
end
|
||||
|
||||
defp do_transform(%{"tuple" => entity}) do
|
||||
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
|
||||
def to_elixir_types(%{"tuple" => entity}) do
|
||||
Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
|
||||
end
|
||||
|
||||
defp do_transform(entity) when is_map(entity) do
|
||||
for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)}
|
||||
def to_elixir_types(entity) when is_map(entity) do
|
||||
Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
|
||||
end
|
||||
|
||||
defp do_transform(entity) when is_list(entity) do
|
||||
for v <- entity, into: [], do: do_transform(v)
|
||||
def to_elixir_types(entity) when is_list(entity) do
|
||||
Enum.map(entity, &to_elixir_types/1)
|
||||
end
|
||||
|
||||
defp do_transform(entity) when is_binary(entity) do
|
||||
def to_elixir_types(entity) when is_binary(entity) do
|
||||
entity
|
||||
|> String.trim()
|
||||
|> do_transform_string()
|
||||
|> string_to_elixir_types()
|
||||
end
|
||||
|
||||
defp do_transform(entity), do: entity
|
||||
def to_elixir_types(entity), do: entity
|
||||
|
||||
@spec string_to_elixir_types(String.t()) ::
|
||||
atom() | Regex.t() | module() | String.t() | no_return()
|
||||
def string_to_elixir_types("~r" <> _pattern = regex) do
|
||||
pattern =
|
||||
~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
|
||||
|
||||
delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
|
||||
|
||||
with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
|
||||
Regex.named_captures(pattern, regex),
|
||||
{:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
|
||||
{result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
|
||||
|
||||
def string_to_elixir_types(value) do
|
||||
if module_name?(value) do
|
||||
String.to_existing_atom("Elixir." <> value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_host("localhost"), do: :localhost
|
||||
|
||||
|
@ -387,27 +373,8 @@ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
|
|||
end
|
||||
end
|
||||
|
||||
defp do_transform_string("~r" <> _pattern = regex) do
|
||||
with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
|
||||
Regex.named_captures(@regex, regex),
|
||||
{:ok, {leading, closing}} <- find_valid_delimiter(@delimiters, pattern, regex_delimiter),
|
||||
{result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_transform_string(":" <> atom), do: String.to_atom(atom)
|
||||
|
||||
defp do_transform_string(value) do
|
||||
if is_module_name?(value) do
|
||||
String.to_existing_atom("Elixir." <> value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_module_name?(String.t()) :: boolean()
|
||||
def is_module_name?(string) do
|
||||
@spec module_name?(String.t()) :: boolean()
|
||||
def module_name?(string) do
|
||||
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
|
||||
string in ["Oban", "Ueberauth", "ExSyslogger"]
|
||||
end
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
|
||||
defmodule Pleroma.Config.DeprecationWarnings do
|
||||
require Logger
|
||||
alias Pleroma.Config
|
||||
|
||||
def check_hellthread_threshold do
|
||||
if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
|
||||
if Config.get([:mrf_hellthread, :threshold]) do
|
||||
Logger.warn("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
You are using the old configuration mechanism for the hellthread filter. Please check config.md.
|
||||
|
@ -14,7 +15,29 @@ def check_hellthread_threshold do
|
|||
end
|
||||
end
|
||||
|
||||
def mrf_user_allowlist do
|
||||
config = Config.get(:mrf_user_allowlist)
|
||||
|
||||
if config && Enum.any?(config, fn {k, _} -> is_atom(k) end) do
|
||||
rewritten =
|
||||
Enum.reduce(Config.get(:mrf_user_allowlist), Map.new(), fn {k, v}, acc ->
|
||||
Map.put(acc, to_string(k), v)
|
||||
end)
|
||||
|
||||
Config.put(:mrf_user_allowlist, rewritten)
|
||||
|
||||
Logger.error("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
As of Pleroma 2.0.7, the `mrf_user_allowlist` setting changed of format.
|
||||
Pleroma 2.1 will remove support for the old format. Please change your configuration to match this:
|
||||
|
||||
config :pleroma, :mrf_user_allowlist, #{inspect(rewritten, pretty: true)}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
def warn do
|
||||
check_hellthread_threshold()
|
||||
mrf_user_allowlist()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,10 +28,6 @@ defmodule Pleroma.Config.TransferTask do
|
|||
{:pleroma, Pleroma.Captcha, [:seconds_valid]},
|
||||
{:pleroma, Pleroma.Upload, [:proxy_remote]},
|
||||
{:pleroma, :instance, [:upload_limit]},
|
||||
{:pleroma, :email_notifications, [:digest]},
|
||||
{:pleroma, :oauth2, [:clean_expired_tokens]},
|
||||
{:pleroma, Pleroma.ActivityExpiration, [:enabled]},
|
||||
{:pleroma, Pleroma.ScheduledActivity, [:enabled]},
|
||||
{:pleroma, :gopher, [:enabled]}
|
||||
]
|
||||
|
||||
|
@ -48,7 +44,7 @@ def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do
|
|||
|
||||
{logger, other} =
|
||||
(Repo.all(ConfigDB) ++ deleted_settings)
|
||||
|> Enum.map(&transform_and_merge/1)
|
||||
|> Enum.map(&merge_with_default/1)
|
||||
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
|
||||
|
||||
logger
|
||||
|
@ -92,11 +88,7 @@ defp maybe_set_pleroma_last(apps) do
|
|||
end
|
||||
end
|
||||
|
||||
defp transform_and_merge(%{group: group, key: key, value: value} = setting) do
|
||||
group = ConfigDB.from_string(group)
|
||||
key = ConfigDB.from_string(key)
|
||||
value = ConfigDB.from_binary(value)
|
||||
|
||||
defp merge_with_default(%{group: group, key: key, value: value} = setting) do
|
||||
default = Config.Holder.default_config(group, key)
|
||||
|
||||
merged =
|
||||
|
|
|
@ -162,10 +162,13 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
|
|||
for_user(user, params)
|
||||
|> Enum.map(fn participation ->
|
||||
activity_id =
|
||||
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
|
||||
"user" => user,
|
||||
"blocking_user" => user
|
||||
})
|
||||
ActivityPub.fetch_latest_direct_activity_id_for_context(
|
||||
participation.conversation.ap_id,
|
||||
%{
|
||||
user: user,
|
||||
blocking_user: user
|
||||
}
|
||||
)
|
||||
|
||||
%{
|
||||
participation
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime do
|
||||
@moduledoc """
|
||||
The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
|
||||
DateTime can't parse this, but it can parse the related iso8601. This
|
|
@ -1,4 +1,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :string
|
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Recipients do
|
||||
use Ecto.Type
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
|
||||
|
||||
def type, do: {:array, ObjectID}
|
||||
|
||||
def cast(object) when is_binary(object) do
|
||||
cast([object])
|
||||
end
|
||||
|
||||
def cast(data) when is_list(data) do
|
||||
data
|
||||
|> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
|
||||
case ObjectID.cast(element) do
|
||||
{:ok, id} ->
|
||||
{:cont, {:ok, [id | list]}}
|
||||
|
||||
_ ->
|
||||
{:halt, :error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def cast(_) do
|
||||
:error
|
||||
end
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
|
@ -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.EctoType.ActivityPub.ObjectValidators.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
|
|
@ -1,4 +1,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.Uri do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :string
|
26
lib/pleroma/ecto_type/config/atom.ex
Normal file
26
lib/pleroma/ecto_type/config/atom.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.Config.Atom do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :atom
|
||||
|
||||
def cast(key) when is_atom(key) do
|
||||
{:ok, key}
|
||||
end
|
||||
|
||||
def cast(key) when is_binary(key) do
|
||||
{:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
def load(key) do
|
||||
{:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
|
||||
end
|
||||
|
||||
def dump(key) when is_atom(key), do: {:ok, inspect(key)}
|
||||
def dump(_), do: :error
|
||||
end
|
27
lib/pleroma/ecto_type/config/binary_value.ex
Normal file
27
lib/pleroma/ecto_type/config/binary_value.ex
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.Config.BinaryValue do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :term
|
||||
|
||||
def cast(value) when is_binary(value) do
|
||||
if String.valid?(value) do
|
||||
{:ok, value}
|
||||
else
|
||||
{:ok, :erlang.binary_to_term(value)}
|
||||
end
|
||||
end
|
||||
|
||||
def cast(value), do: {:ok, value}
|
||||
|
||||
def load(value) when is_binary(value) do
|
||||
{:ok, :erlang.binary_to_term(value)}
|
||||
end
|
||||
|
||||
def dump(value) do
|
||||
{:ok, :erlang.term_to_binary(value)}
|
||||
end
|
||||
end
|
|
@ -141,6 +141,12 @@ def following_query(%User{} = user) do
|
|||
|> where([r], r.state == ^:follow_accept)
|
||||
end
|
||||
|
||||
def outgoing_pending_follow_requests_query(%User{} = follower) do
|
||||
__MODULE__
|
||||
|> where([r], r.follower_id == ^follower.id)
|
||||
|> where([r], r.state == ^:follow_pending)
|
||||
end
|
||||
|
||||
def following(%User{} = user) do
|
||||
following =
|
||||
following_query(user)
|
||||
|
|
85
lib/pleroma/migration_helper/notification_backfill.ex
Normal file
85
lib/pleroma/migration_helper/notification_backfill.ex
Normal 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.chunk_stream(100)
|
||||
|> 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
|
|
@ -30,12 +30,29 @@ defmodule Pleroma.Notification do
|
|||
|
||||
schema "notifications" do
|
||||
field(:seen, :boolean, default: false)
|
||||
# This is an enum type in the database. If you add a new notification type,
|
||||
# remember to add a migration to add it to the `notifications_type` enum
|
||||
# as well.
|
||||
field(:type, :string)
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def update_notification_type(user, activity) do
|
||||
with %__MODULE__{} = notification <-
|
||||
Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
|
||||
type =
|
||||
activity
|
||||
|> type_from_activity()
|
||||
|
||||
notification
|
||||
|> changeset(%{type: type})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@spec unread_notifications_count(User.t()) :: integer()
|
||||
def unread_notifications_count(%User{id: user_id}) do
|
||||
from(q in __MODULE__,
|
||||
|
@ -44,9 +61,21 @@ def unread_notifications_count(%User{id: user_id}) do
|
|||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
@notification_types ~w{
|
||||
favourite
|
||||
follow
|
||||
follow_request
|
||||
mention
|
||||
move
|
||||
pleroma:chat_mention
|
||||
pleroma:emoji_reaction
|
||||
reblog
|
||||
}
|
||||
|
||||
def changeset(%Notification{} = notification, attrs) do
|
||||
notification
|
||||
|> cast(attrs, [:seen])
|
||||
|> cast(attrs, [:seen, :type])
|
||||
|> validate_inclusion(:type, @notification_types)
|
||||
end
|
||||
|
||||
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
|
||||
|
@ -137,8 +166,16 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
|||
query
|
||||
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
|
||||
on:
|
||||
fragment("?->>'context'", a.data) ==
|
||||
fragment("?->>'context'", mutated_activity.data) and
|
||||
fragment(
|
||||
"COALESCE((?->'object')->>'id', ?->>'object')",
|
||||
a.data,
|
||||
a.data
|
||||
) ==
|
||||
fragment(
|
||||
"COALESCE((?->'object')->>'id', ?->>'object')",
|
||||
mutated_activity.data,
|
||||
mutated_activity.data
|
||||
) and
|
||||
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
|
||||
fragment("?->>'type'", mutated_activity.data) == "Create",
|
||||
as: :mutated_activity
|
||||
|
@ -300,42 +337,95 @@ def dismiss(%{id: user_id} = _user, id) do
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
|
||||
object = Object.normalize(activity)
|
||||
def create_notifications(activity, options \\ [])
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
if object && object.data["type"] == "Answer" do
|
||||
{:ok, []}
|
||||
else
|
||||
do_create_notifications(activity)
|
||||
do_create_notifications(activity, options)
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
|
||||
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
|
||||
do_create_notifications(activity)
|
||||
do_create_notifications(activity, options)
|
||||
end
|
||||
|
||||
def create_notifications(_), do: {:ok, []}
|
||||
def create_notifications(_, _), do: {:ok, []}
|
||||
|
||||
defp do_create_notifications(%Activity{} = activity, options) do
|
||||
do_send = Keyword.get(options, :do_send, true)
|
||||
|
||||
defp do_create_notifications(%Activity{} = activity) do
|
||||
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
|
||||
potential_receivers = enabled_receivers ++ disabled_receivers
|
||||
|
||||
notifications =
|
||||
Enum.map(potential_receivers, fn user ->
|
||||
do_send = user in enabled_receivers
|
||||
do_send = do_send && user in enabled_receivers
|
||||
create_notification(activity, user, do_send)
|
||||
end)
|
||||
|
||||
{:ok, notifications}
|
||||
end
|
||||
|
||||
defp type_from_activity(%{data: %{"type" => type}} = activity) do
|
||||
case type do
|
||||
"Follow" ->
|
||||
if Activity.follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
|
||||
"Announce" ->
|
||||
"reblog"
|
||||
|
||||
"Like" ->
|
||||
"favourite"
|
||||
|
||||
"Move" ->
|
||||
"move"
|
||||
|
||||
"EmojiReact" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
# Compatibility with old reactions
|
||||
"EmojiReaction" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
"Create" ->
|
||||
activity
|
||||
|> type_from_activity_object()
|
||||
|
||||
t ->
|
||||
raise "No notification type for activity type #{t}"
|
||||
end
|
||||
end
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
|
||||
object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
case object && object.data["type"] do
|
||||
"ChatMessage" -> "pleroma:chat_mention"
|
||||
_ -> "mention"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO move to sql, too.
|
||||
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
|
||||
unless skip?(activity, user) do
|
||||
{:ok, %{notification: notification}} =
|
||||
Multi.new()
|
||||
|> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
|
||||
|> Multi.insert(:notification, %Notification{
|
||||
user_id: user.id,
|
||||
activity: activity,
|
||||
type: type_from_activity(activity)
|
||||
})
|
||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
|
||||
|
@ -459,6 +549,7 @@ def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
|
|||
def skip?(%Activity{} = activity, %User{} = user) do
|
||||
[
|
||||
:self,
|
||||
:invisible,
|
||||
:followers,
|
||||
:follows,
|
||||
:non_followers,
|
||||
|
@ -475,6 +566,12 @@ def skip?(:self, %Activity{} = activity, %User{} = user) do
|
|||
activity.data["actor"] == user.ap_id
|
||||
end
|
||||
|
||||
def skip?(:invisible, %Activity{} = activity, _) do
|
||||
actor = activity.data["actor"]
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
User.invisible?(user)
|
||||
end
|
||||
|
||||
def skip?(
|
||||
:followers,
|
||||
%Activity{} = activity,
|
||||
|
@ -527,4 +624,12 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
|
|||
end
|
||||
|
||||
def skip?(_, _, _), do: false
|
||||
|
||||
def for_user_and_activity(user, activity) do
|
||||
from(n in __MODULE__,
|
||||
where: n.user_id == ^user.id,
|
||||
where: n.activity_id == ^activity.id
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,12 +23,12 @@ def page_keys, do: @page_keys
|
|||
@spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
|
||||
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
|
||||
|
||||
def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
|
||||
def fetch_paginated(query, %{total: true} = params, :keyset, table_binding) do
|
||||
total = Repo.aggregate(query, :count, :id)
|
||||
|
||||
%{
|
||||
total: total,
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
|
||||
items: fetch_paginated(query, Map.drop(params, [:total]), :keyset, table_binding)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -41,7 +41,7 @@ def fetch_paginated(query, params, :keyset, table_binding) do
|
|||
|> enforce_order(options)
|
||||
end
|
||||
|
||||
def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
|
||||
def fetch_paginated(query, %{total: true} = params, :offset, table_binding) do
|
||||
total =
|
||||
query
|
||||
|> Ecto.Query.exclude(:left_join)
|
||||
|
@ -49,7 +49,7 @@ def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding)
|
|||
|
||||
%{
|
||||
total: total,
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
|
||||
items: fetch_paginated(query, Map.drop(params, [:total]), :offset, table_binding)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -90,12 +90,6 @@ defp cast_params(params) do
|
|||
skip_order: :boolean
|
||||
}
|
||||
|
||||
params =
|
||||
Enum.reduce(params, %{}, fn
|
||||
{key, _value}, acc when is_atom(key) -> Map.drop(acc, [key])
|
||||
{key, value}, acc -> Map.put(acc, key, value)
|
||||
end)
|
||||
|
||||
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
|
||||
changeset.changes
|
||||
end
|
||||
|
|
|
@ -113,6 +113,10 @@ defp get_proxy_and_attachment_sources do
|
|||
add_source(acc, host)
|
||||
end)
|
||||
|
||||
media_proxy_base_url =
|
||||
if Config.get([:media_proxy, :base_url]),
|
||||
do: URI.parse(Config.get([:media_proxy, :base_url])).host
|
||||
|
||||
upload_base_url =
|
||||
if Config.get([Pleroma.Upload, :base_url]),
|
||||
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
|
||||
|
@ -122,6 +126,7 @@ defp get_proxy_and_attachment_sources do
|
|||
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
|
||||
|
||||
[]
|
||||
|> add_source(media_proxy_base_url)
|
||||
|> add_source(upload_base_url)
|
||||
|> add_source(s3_endpoint)
|
||||
|> add_source(media_proxy_whitelist)
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Repo do
|
|||
adapter: Ecto.Adapters.Postgres,
|
||||
migration_timestamps: [type: :naive_datetime_usec]
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
defmodule Instrumenter do
|
||||
|
@ -78,6 +79,33 @@ def check_migrations_applied!() do
|
|||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def chunk_stream(query, chunk_size) do
|
||||
# We don't actually need start and end funcitons of resource streaming,
|
||||
# but it seems to be the only way to not fetch records one-by-one and
|
||||
# have individual records be the elements of the stream, instead of
|
||||
# lists of records
|
||||
Stream.resource(
|
||||
fn -> 0 end,
|
||||
fn
|
||||
last_id ->
|
||||
query
|
||||
|> order_by(asc: :id)
|
||||
|> where([r], r.id > ^last_id)
|
||||
|> limit(^chunk_size)
|
||||
|> all()
|
||||
|> case do
|
||||
[] ->
|
||||
{:halt, last_id}
|
||||
|
||||
records ->
|
||||
last_id = List.last(records).id
|
||||
{records, last_id}
|
||||
end
|
||||
end,
|
||||
fn _ -> :ok end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Pleroma.Repo.UnappliedMigrationsError do
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
defmodule Pleroma.Signature do
|
||||
@behaviour HTTPSignatures.Adapter
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
def key_id_to_actor_id(key_id) do
|
||||
uri =
|
||||
|
@ -24,7 +24,7 @@ def key_id_to_actor_id(key_id) do
|
|||
|
||||
maybe_ap_id = URI.to_string(uri)
|
||||
|
||||
case Types.ObjectID.cast(maybe_ap_id) do
|
||||
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
|
||||
{:ok, ap_id} ->
|
||||
{:ok, ap_id}
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ def store(upload, opts \\ []) do
|
|||
{:ok,
|
||||
%{
|
||||
"type" => opts.activity_type,
|
||||
"mediaType" => upload.content_type,
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Delivery
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Formatter
|
||||
|
@ -30,7 +31,6 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
@ -79,6 +79,7 @@ defmodule Pleroma.User do
|
|||
|
||||
schema "users" do
|
||||
field(:bio, :string)
|
||||
field(:raw_bio, :string)
|
||||
field(:email, :string)
|
||||
field(:name, :string)
|
||||
field(:nickname, :string)
|
||||
|
@ -115,7 +116,7 @@ defmodule Pleroma.User do
|
|||
field(:is_admin, :boolean, default: false)
|
||||
field(:show_role, :boolean, default: true)
|
||||
field(:settings, :map, default: nil)
|
||||
field(:uri, Types.Uri, default: nil)
|
||||
field(:uri, ObjectValidators.Uri, default: nil)
|
||||
field(:hide_followers_count, :boolean, default: false)
|
||||
field(:hide_follows_count, :boolean, default: false)
|
||||
field(:hide_followers, :boolean, default: false)
|
||||
|
@ -432,6 +433,7 @@ def update_changeset(struct, params \\ %{}) do
|
|||
params,
|
||||
[
|
||||
:bio,
|
||||
:raw_bio,
|
||||
:name,
|
||||
:emoji,
|
||||
:avatar,
|
||||
|
@ -607,7 +609,16 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
|
||||
struct
|
||||
|> confirmation_changeset(need_confirmation: need_confirmation?)
|
||||
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
|
||||
|> cast(params, [
|
||||
:bio,
|
||||
:raw_bio,
|
||||
:email,
|
||||
:name,
|
||||
:nickname,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:emoji
|
||||
])
|
||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|
@ -1488,6 +1499,9 @@ def perform(:delete, %User{} = user) do
|
|||
end)
|
||||
|
||||
delete_user_activities(user)
|
||||
delete_notifications_from_user_activities(user)
|
||||
|
||||
delete_outgoing_pending_follow_requests(user)
|
||||
|
||||
delete_or_deactivate(user)
|
||||
end
|
||||
|
@ -1574,6 +1588,13 @@ def follow_import(%User{} = follower, followed_identifiers)
|
|||
})
|
||||
end
|
||||
|
||||
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
|
||||
Notification
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_user_activities(%User{ap_id: ap_id} = user) do
|
||||
ap_id
|
||||
|> Activity.Queries.by_actor()
|
||||
|
@ -1611,6 +1632,12 @@ defp delete_activity(%{data: %{"type" => type}} = activity, user)
|
|||
|
||||
defp delete_activity(_activity, _user), do: "Doing nothing"
|
||||
|
||||
defp delete_outgoing_pending_follow_requests(user) do
|
||||
user
|
||||
|> FollowingRelationship.outgoing_pending_follow_requests_query()
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def html_filter_policy(%User{no_rich_text: true}) do
|
||||
Pleroma.HTML.Scrubber.TwitterText
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Activity.Ir.Topics
|
||||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Constants
|
||||
alias Pleroma.Conversation
|
||||
|
@ -31,25 +32,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
# For Announce activities, we filter the recipients based on following status for any actors
|
||||
# that match actual users. See issue #164 for more information about why this is necessary.
|
||||
defp get_recipients(%{"type" => "Announce"} = data) do
|
||||
to = Map.get(data, "to", [])
|
||||
cc = Map.get(data, "cc", [])
|
||||
bcc = Map.get(data, "bcc", [])
|
||||
actor = User.get_cached_by_ap_id(data["actor"])
|
||||
|
||||
recipients =
|
||||
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
|
||||
case User.get_cached_by_ap_id(recipient) do
|
||||
nil -> true
|
||||
user -> User.following?(user, actor)
|
||||
end
|
||||
end)
|
||||
|
||||
{recipients, to, cc}
|
||||
end
|
||||
|
||||
defp get_recipients(%{"type" => "Create"} = data) do
|
||||
to = Map.get(data, "to", [])
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
@ -67,16 +49,12 @@ defp get_recipients(data) do
|
|||
{recipients, to, cc}
|
||||
end
|
||||
|
||||
defp check_actor_is_active(actor) do
|
||||
if not is_nil(actor) do
|
||||
with user <- User.get_cached_by_ap_id(actor),
|
||||
false <- user.deactivated do
|
||||
true
|
||||
else
|
||||
_e -> false
|
||||
end
|
||||
else
|
||||
true
|
||||
defp check_actor_is_active(nil), do: true
|
||||
|
||||
defp check_actor_is_active(actor) when is_binary(actor) do
|
||||
case User.get_cached_by_ap_id(actor) do
|
||||
%User{deactivated: deactivated} -> not deactivated
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -87,7 +65,7 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(
|
|||
|
||||
defp check_remote_limit(_), do: true
|
||||
|
||||
def increase_note_count_if_public(actor, object) do
|
||||
defp increase_note_count_if_public(actor, object) do
|
||||
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
|
@ -95,7 +73,7 @@ def decrease_note_count_if_public(actor, object) do
|
|||
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def increase_replies_count_if_reply(%{
|
||||
defp increase_replies_count_if_reply(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
||||
"type" => "Create"
|
||||
}) do
|
||||
|
@ -104,19 +82,9 @@ def increase_replies_count_if_reply(%{
|
|||
end
|
||||
end
|
||||
|
||||
def increase_replies_count_if_reply(_create_data), do: :noop
|
||||
defp increase_replies_count_if_reply(_create_data), do: :noop
|
||||
|
||||
def decrease_replies_count_if_reply(%Object{
|
||||
data: %{"inReplyTo" => reply_ap_id} = object
|
||||
}) do
|
||||
if is_public?(object) do
|
||||
Object.decrease_replies_count(reply_ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
def decrease_replies_count_if_reply(_object), do: :noop
|
||||
|
||||
def increase_poll_votes_if_vote(%{
|
||||
defp increase_poll_votes_if_vote(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
|
||||
"type" => "Create",
|
||||
"actor" => actor
|
||||
|
@ -124,9 +92,16 @@ def increase_poll_votes_if_vote(%{
|
|||
Object.increase_vote_count(reply_ap_id, name, actor)
|
||||
end
|
||||
|
||||
def increase_poll_votes_if_vote(_create_data), do: :noop
|
||||
defp increase_poll_votes_if_vote(_create_data), do: :noop
|
||||
|
||||
@object_types ["ChatMessage"]
|
||||
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
|
||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
||||
with {:ok, object} <- Object.create(object) do
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def persist(object, meta) do
|
||||
with local <- Keyword.fetch!(meta, :local),
|
||||
{recipients, _, _} <- get_recipients(object),
|
||||
|
@ -153,12 +128,14 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
|||
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
|
||||
{:ok, map, object} <- insert_full_object(map) do
|
||||
{:ok, activity} =
|
||||
Repo.insert(%Activity{
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: recipients
|
||||
})
|
||||
}
|
||||
|> Repo.insert()
|
||||
|> maybe_create_activity_expiration()
|
||||
|
||||
# Splice in the child object if we have one.
|
||||
activity = Maps.put_if_present(activity, :object, object)
|
||||
|
@ -196,10 +173,18 @@ def notify_and_stream(activity) do
|
|||
stream_out_participations(participations)
|
||||
end
|
||||
|
||||
defp maybe_create_activity_expiration({:ok, %{data: %{"expires_at" => expires_at}} = activity}) do
|
||||
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_create_activity_expiration(result), do: result
|
||||
|
||||
defp create_or_bump_conversation(activity, actor) do
|
||||
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor),
|
||||
Participation.mark_as_read(user, conversation) do
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
Participation.mark_as_read(user, conversation)
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
@ -221,13 +206,15 @@ def stream_out_participations(participations) do
|
|||
end
|
||||
|
||||
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
|
||||
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
|
||||
conversation = Repo.preload(conversation, :participations),
|
||||
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
|
||||
conversation = Repo.preload(conversation, :participations)
|
||||
|
||||
last_activity_id =
|
||||
fetch_latest_activity_id_for_context(conversation.ap_id, %{
|
||||
"user" => user,
|
||||
"blocking_user" => user
|
||||
}) do
|
||||
fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
|
||||
user: user,
|
||||
blocking_user: user
|
||||
})
|
||||
|
||||
if last_activity_id do
|
||||
stream_out_participations(conversation.participations)
|
||||
end
|
||||
|
@ -261,12 +248,13 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
|
|||
published = params[:published]
|
||||
quick_insert? = Config.get([:env]) == :benchmark
|
||||
|
||||
with create_data <-
|
||||
create_data =
|
||||
make_create_data(
|
||||
%{to: to, actor: actor, published: published, context: context, object: object},
|
||||
additional
|
||||
),
|
||||
{:ok, activity} <- insert(create_data, local, fake),
|
||||
)
|
||||
|
||||
with {:ok, activity} <- insert(create_data, local, fake),
|
||||
{:fake, false, activity} <- {:fake, fake, activity},
|
||||
_ <- increase_replies_count_if_reply(create_data),
|
||||
_ <- increase_poll_votes_if_vote(create_data),
|
||||
|
@ -294,12 +282,13 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
|
|||
local = !(params[:local] == false)
|
||||
published = params[:published]
|
||||
|
||||
with listen_data <-
|
||||
listen_data =
|
||||
make_listen_data(
|
||||
%{to: to, actor: actor, published: published, context: context, object: object},
|
||||
additional
|
||||
),
|
||||
{:ok, activity} <- insert(listen_data, local),
|
||||
)
|
||||
|
||||
with {:ok, activity} <- insert(listen_data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
@ -317,14 +306,15 @@ def reject(params) do
|
|||
end
|
||||
|
||||
@spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
|
||||
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||
defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||
local = Map.get(params, :local, true)
|
||||
activity_id = Map.get(params, :activity_id, nil)
|
||||
|
||||
with data <-
|
||||
data =
|
||||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
||||
|> Maps.put_if_present("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
|> Maps.put_if_present("id", activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
@ -336,34 +326,38 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
|||
local = !(params[:local] == false)
|
||||
activity_id = params[:activity_id]
|
||||
|
||||
with data <- %{
|
||||
data =
|
||||
%{
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"type" => "Update",
|
||||
"actor" => actor,
|
||||
"object" => object
|
||||
},
|
||||
data <- Maps.put_if_present(data, "id", activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
}
|
||||
|> Maps.put_if_present("id", activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
|
||||
with {:ok, result} <-
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_follow(follower, followed, activity_id, local) do
|
||||
with data <- make_follow_data(follower, followed, activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
defp do_follow(follower, followed, activity_id, local, opts) do
|
||||
skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
|
||||
data = make_follow_data(follower, followed, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- skip_notify_and_stream || notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
@ -406,13 +400,13 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do
|
|||
defp do_block(blocker, blocked, activity_id, local) do
|
||||
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
|
||||
|
||||
if unfollow_blocked do
|
||||
follow_activity = fetch_latest_follow(blocker, blocked)
|
||||
if follow_activity, do: unfollow(blocker, blocked, nil, local)
|
||||
if unfollow_blocked and fetch_latest_follow(blocker, blocked) do
|
||||
unfollow(blocker, blocked, nil, local)
|
||||
end
|
||||
|
||||
with block_data <- make_block_data(blocker, blocked, activity_id),
|
||||
{:ok, activity} <- insert(block_data, local),
|
||||
block_data = make_block_data(blocker, blocked, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(block_data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
@ -491,8 +485,8 @@ def fetch_activities_for_context_query(context, opts) do
|
|||
public = [Constants.as_public()]
|
||||
|
||||
recipients =
|
||||
if opts["user"],
|
||||
do: [opts["user"].ap_id | User.following(opts["user"])] ++ public,
|
||||
if opts[:user],
|
||||
do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
|
||||
else: public
|
||||
|
||||
from(activity in Activity)
|
||||
|
@ -500,7 +494,7 @@ def fetch_activities_for_context_query(context, opts) do
|
|||
|> maybe_preload_bookmarks(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_recipients(recipients, opts["user"])
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> where(
|
||||
[activity],
|
||||
fragment(
|
||||
|
@ -523,11 +517,12 @@ def fetch_activities_for_context(context, opts \\ %{}) do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
|
||||
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
|
||||
FlakeId.Ecto.CompatType.t() | nil
|
||||
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|
||||
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
|
||||
context
|
||||
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|
||||
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
|
||||
|> restrict_visibility(%{visibility: "direct"})
|
||||
|> limit(1)
|
||||
|> select([a], a.id)
|
||||
|> Repo.one()
|
||||
|
@ -535,24 +530,18 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|
|||
|
||||
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||
opts = Map.drop(opts, ["user"])
|
||||
opts = Map.delete(opts, :user)
|
||||
|
||||
query = fetch_activities_query([Constants.as_public()], opts)
|
||||
|
||||
query =
|
||||
if opts["restrict_unlisted"] do
|
||||
restrict_unlisted(query)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
Pagination.fetch_paginated(query, opts, pagination)
|
||||
[Constants.as_public()]
|
||||
|> fetch_activities_query(opts)
|
||||
|> restrict_unlisted(opts)
|
||||
|> Pagination.fetch_paginated(opts, pagination)
|
||||
end
|
||||
|
||||
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||
opts
|
||||
|> Map.put("restrict_unlisted", true)
|
||||
|> Map.put(:restrict_unlisted, true)
|
||||
|> fetch_public_or_unlisted_activities(pagination)
|
||||
end
|
||||
|
||||
|
@ -561,7 +550,6 @@ def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
|
|||
defp restrict_visibility(query, %{visibility: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
query =
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
|
@ -573,8 +561,6 @@ defp restrict_visibility(query, %{visibility: visibility})
|
|||
^visibility
|
||||
)
|
||||
)
|
||||
|
||||
query
|
||||
else
|
||||
Logger.error("Could not restrict visibility to #{visibility}")
|
||||
end
|
||||
|
@ -596,7 +582,7 @@ defp restrict_visibility(_query, %{visibility: visibility})
|
|||
|
||||
defp restrict_visibility(query, _visibility), do: query
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
from(
|
||||
|
@ -616,7 +602,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
|||
end
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
from(
|
||||
a in query,
|
||||
|
@ -631,7 +617,7 @@ defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
|||
)
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility not in [nil | @valid_visibilities] do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
|
@ -642,14 +628,10 @@ defp exclude_visibility(query, _visibility), do: query
|
|||
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
|
||||
do: query
|
||||
|
||||
defp restrict_thread_visibility(
|
||||
query,
|
||||
%{"user" => %User{skip_thread_containment: true}},
|
||||
_
|
||||
),
|
||||
defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _),
|
||||
do: query
|
||||
|
||||
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
|
||||
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
|
||||
from(
|
||||
a in query,
|
||||
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
|
||||
|
@ -661,87 +643,99 @@ defp restrict_thread_visibility(query, _, _), do: query
|
|||
def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("user", reading_user)
|
||||
|> Map.put("actor_id", user.ap_id)
|
||||
|> Map.put(:user, reading_user)
|
||||
|> Map.put(:actor_id, user.ap_id)
|
||||
|
||||
recipients =
|
||||
user_activities_recipients(%{
|
||||
"godmode" => params["godmode"],
|
||||
"reading_user" => reading_user
|
||||
})
|
||||
|
||||
fetch_activities(recipients, params)
|
||||
%{
|
||||
godmode: params[:godmode],
|
||||
reading_user: reading_user
|
||||
}
|
||||
|> user_activities_recipients()
|
||||
|> fetch_activities(params)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def fetch_user_activities(user, reading_user, params \\ %{}) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("user", reading_user)
|
||||
|> Map.put("actor_id", user.ap_id)
|
||||
|> Map.put("pinned_activity_ids", user.pinned_activities)
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|> Map.put(:user, reading_user)
|
||||
|> Map.put(:actor_id, user.ap_id)
|
||||
|> Map.put(:pinned_activity_ids, user.pinned_activities)
|
||||
|
||||
params =
|
||||
if User.blocks?(reading_user, user) do
|
||||
params
|
||||
else
|
||||
params
|
||||
|> Map.put("blocking_user", reading_user)
|
||||
|> Map.put("muting_user", reading_user)
|
||||
|> Map.put(:blocking_user, reading_user)
|
||||
|> Map.put(:muting_user, reading_user)
|
||||
end
|
||||
|
||||
recipients =
|
||||
user_activities_recipients(%{
|
||||
"godmode" => params["godmode"],
|
||||
"reading_user" => reading_user
|
||||
})
|
||||
|
||||
fetch_activities(recipients, params)
|
||||
%{
|
||||
godmode: params[:godmode],
|
||||
reading_user: reading_user
|
||||
}
|
||||
|> user_activities_recipients()
|
||||
|> fetch_activities(params)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def fetch_statuses(reading_user, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
params = Map.put(params, :type, ["Create", "Announce"])
|
||||
|
||||
recipients =
|
||||
user_activities_recipients(%{
|
||||
"godmode" => params["godmode"],
|
||||
"reading_user" => reading_user
|
||||
})
|
||||
|
||||
fetch_activities(recipients, params, :offset)
|
||||
%{
|
||||
godmode: params[:godmode],
|
||||
reading_user: reading_user
|
||||
}
|
||||
|> user_activities_recipients()
|
||||
|> fetch_activities(params, :offset)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp user_activities_recipients(%{"godmode" => true}) do
|
||||
[]
|
||||
end
|
||||
defp user_activities_recipients(%{godmode: true}), do: []
|
||||
|
||||
defp user_activities_recipients(%{"reading_user" => reading_user}) do
|
||||
defp user_activities_recipients(%{reading_user: reading_user}) do
|
||||
if reading_user do
|
||||
[Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
|
||||
[Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
|
||||
else
|
||||
[Constants.as_public()]
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_since(query, %{"since_id" => ""}), do: query
|
||||
defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
|
||||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_since(query, %{"since_id" => since_id}) do
|
||||
defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
where:
|
||||
fragment(
|
||||
"?->>'type' != ? or ?->>'actor' != ?",
|
||||
activity.data,
|
||||
"Announce",
|
||||
object.data,
|
||||
^actor
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_announce_object_actor(query, _), do: query
|
||||
|
||||
defp restrict_since(query, %{since_id: ""}), do: query
|
||||
|
||||
defp restrict_since(query, %{since_id: since_id}) do
|
||||
from(activity in query, where: activity.id > ^since_id)
|
||||
end
|
||||
|
||||
defp restrict_since(query, _), do: query
|
||||
|
||||
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
|
||||
defp restrict_tag_reject(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
|
||||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
|
||||
when is_list(tag_reject) and tag_reject != [] do
|
||||
defp restrict_tag_reject(query, %{tag_reject: [_ | _] = tag_reject}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
|
||||
|
@ -750,12 +744,11 @@ defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
|
|||
|
||||
defp restrict_tag_reject(query, _), do: query
|
||||
|
||||
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
|
||||
defp restrict_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
|
||||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_tag_all(query, %{"tag_all" => tag_all})
|
||||
when is_list(tag_all) and tag_all != [] do
|
||||
defp restrict_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
|
||||
|
@ -764,18 +757,18 @@ defp restrict_tag_all(query, %{"tag_all" => tag_all})
|
|||
|
||||
defp restrict_tag_all(query, _), do: query
|
||||
|
||||
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
|
||||
defp restrict_tag(_query, %{tag: _tag, skip_preload: true}) do
|
||||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
|
||||
defp restrict_tag(query, %{tag: tag}) when is_list(tag) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
|
||||
defp restrict_tag(query, %{tag: tag}) when is_binary(tag) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
|
||||
|
@ -798,35 +791,35 @@ defp restrict_recipients(query, recipients, user) do
|
|||
)
|
||||
end
|
||||
|
||||
defp restrict_local(query, %{"local_only" => true}) do
|
||||
defp restrict_local(query, %{local_only: true}) do
|
||||
from(activity in query, where: activity.local == true)
|
||||
end
|
||||
|
||||
defp restrict_local(query, _), do: query
|
||||
|
||||
defp restrict_actor(query, %{"actor_id" => actor_id}) do
|
||||
defp restrict_actor(query, %{actor_id: actor_id}) do
|
||||
from(activity in query, where: activity.actor == ^actor_id)
|
||||
end
|
||||
|
||||
defp restrict_actor(query, _), do: query
|
||||
|
||||
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
|
||||
defp restrict_type(query, %{type: type}) when is_binary(type) do
|
||||
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
|
||||
end
|
||||
|
||||
defp restrict_type(query, %{"type" => type}) do
|
||||
defp restrict_type(query, %{type: type}) do
|
||||
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
|
||||
end
|
||||
|
||||
defp restrict_type(query, _), do: query
|
||||
|
||||
defp restrict_state(query, %{"state" => state}) do
|
||||
defp restrict_state(query, %{state: state}) do
|
||||
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
|
||||
end
|
||||
|
||||
defp restrict_state(query, _), do: query
|
||||
|
||||
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
|
||||
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
|
||||
|
@ -835,11 +828,11 @@ defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
|
|||
|
||||
defp restrict_favorited_by(query, _), do: query
|
||||
|
||||
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
|
||||
defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do
|
||||
raise "Can't use the child object without preloading!"
|
||||
end
|
||||
|
||||
defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1"] do
|
||||
defp restrict_media(query, %{only_media: true}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
|
||||
|
@ -848,7 +841,7 @@ defp restrict_media(query, %{"only_media" => val}) when val in [true, "true", "1
|
|||
|
||||
defp restrict_media(query, _), do: query
|
||||
|
||||
defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "true", "1"] do
|
||||
defp restrict_replies(query, %{exclude_replies: true}) do
|
||||
from(
|
||||
[_activity, object] in query,
|
||||
where: fragment("?->>'inReplyTo' is null", object.data)
|
||||
|
@ -856,8 +849,8 @@ defp restrict_replies(query, %{"exclude_replies" => val}) when val in [true, "tr
|
|||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
"reply_filtering_user" => user,
|
||||
"reply_visibility" => "self"
|
||||
reply_filtering_user: user,
|
||||
reply_visibility: "self"
|
||||
}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
|
@ -872,8 +865,8 @@ defp restrict_replies(query, %{
|
|||
end
|
||||
|
||||
defp restrict_replies(query, %{
|
||||
"reply_filtering_user" => user,
|
||||
"reply_visibility" => "following"
|
||||
reply_filtering_user: user,
|
||||
reply_visibility: "following"
|
||||
}) do
|
||||
from(
|
||||
[activity, object] in query,
|
||||
|
@ -892,16 +885,16 @@ defp restrict_replies(query, %{
|
|||
|
||||
defp restrict_replies(query, _), do: query
|
||||
|
||||
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val in [true, "true", "1"] do
|
||||
defp restrict_reblogs(query, %{exclude_reblogs: true}) do
|
||||
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
|
||||
end
|
||||
|
||||
defp restrict_reblogs(query, _), do: query
|
||||
|
||||
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
|
||||
defp restrict_muted(query, %{with_muted: true}), do: query
|
||||
|
||||
defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
|
||||
mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
|
||||
defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do
|
||||
mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user)
|
||||
|
||||
query =
|
||||
from([activity] in query,
|
||||
|
@ -909,7 +902,7 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
|
|||
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
|
||||
)
|
||||
|
||||
unless opts["skip_preload"] do
|
||||
unless opts[:skip_preload] do
|
||||
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
|
||||
else
|
||||
query
|
||||
|
@ -918,8 +911,8 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
|
|||
|
||||
defp restrict_muted(query, _), do: query
|
||||
|
||||
defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
|
||||
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
|
||||
defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
|
||||
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
||||
domain_blocks = user.domain_blocks || []
|
||||
|
||||
following_ap_ids = User.get_friends_ap_ids(user)
|
||||
|
@ -965,7 +958,7 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
|
|||
|
||||
defp restrict_blocked(query, _), do: query
|
||||
|
||||
defp restrict_unlisted(query) do
|
||||
defp restrict_unlisted(query, %{restrict_unlisted: true}) do
|
||||
from(
|
||||
activity in query,
|
||||
where:
|
||||
|
@ -977,19 +970,16 @@ defp restrict_unlisted(query) do
|
|||
)
|
||||
end
|
||||
|
||||
# TODO: when all endpoints migrated to OpenAPI compare `pinned` with `true` (boolean) only,
|
||||
# the same for `restrict_media/2`, `restrict_replies/2`, 'restrict_reblogs/2'
|
||||
# and `restrict_muted/2`
|
||||
defp restrict_unlisted(query, _), do: query
|
||||
|
||||
defp restrict_pinned(query, %{"pinned" => pinned, "pinned_activity_ids" => ids})
|
||||
when pinned in [true, "true", "1"] do
|
||||
defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do
|
||||
from(activity in query, where: activity.id in ^ids)
|
||||
end
|
||||
|
||||
defp restrict_pinned(query, _), do: query
|
||||
|
||||
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
|
||||
muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
|
||||
defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do
|
||||
muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user)
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
|
@ -1005,7 +995,7 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
|
|||
|
||||
defp restrict_muted_reblogs(query, _), do: query
|
||||
|
||||
defp restrict_instance(query, %{"instance" => instance}) do
|
||||
defp restrict_instance(query, %{instance: instance}) do
|
||||
users =
|
||||
from(
|
||||
u in User,
|
||||
|
@ -1019,7 +1009,7 @@ defp restrict_instance(query, %{"instance" => instance}) do
|
|||
|
||||
defp restrict_instance(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
if has_named_binding?(query, :object) do
|
||||
|
@ -1031,7 +1021,19 @@ defp exclude_poll_votes(query, _) do
|
|||
end
|
||||
end
|
||||
|
||||
defp exclude_invisible_actors(query, %{"invisible_actors" => true}), do: query
|
||||
defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
|
||||
|
||||
defp exclude_chat_messages(query, _) do
|
||||
if has_named_binding?(query, :object) do
|
||||
from([activity, object: o] in query,
|
||||
where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
|
||||
)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
|
||||
|
||||
defp exclude_invisible_actors(query, _opts) do
|
||||
invisible_ap_ids =
|
||||
|
@ -1042,38 +1044,38 @@ defp exclude_invisible_actors(query, _opts) do
|
|||
from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
|
||||
end
|
||||
|
||||
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
|
||||
defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do
|
||||
from(activity in query, where: activity.id != ^id)
|
||||
end
|
||||
|
||||
defp exclude_id(query, _), do: query
|
||||
|
||||
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
|
||||
defp maybe_preload_objects(query, %{skip_preload: true}), do: query
|
||||
|
||||
defp maybe_preload_objects(query, _) do
|
||||
query
|
||||
|> Activity.with_preloaded_object()
|
||||
end
|
||||
|
||||
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
|
||||
defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query
|
||||
|
||||
defp maybe_preload_bookmarks(query, opts) do
|
||||
query
|
||||
|> Activity.with_preloaded_bookmark(opts["user"])
|
||||
|> Activity.with_preloaded_bookmark(opts[:user])
|
||||
end
|
||||
|
||||
defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
|
||||
defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do
|
||||
query
|
||||
|> Activity.with_preloaded_report_notes()
|
||||
end
|
||||
|
||||
defp maybe_preload_report_notes(query, _), do: query
|
||||
|
||||
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
|
||||
defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query
|
||||
|
||||
defp maybe_set_thread_muted_field(query, opts) do
|
||||
query
|
||||
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
|
||||
|> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user])
|
||||
end
|
||||
|
||||
defp maybe_order(query, %{order: :desc}) do
|
||||
|
@ -1089,24 +1091,23 @@ defp maybe_order(query, %{order: :asc}) do
|
|||
defp maybe_order(query, _), do: query
|
||||
|
||||
defp fetch_activities_query_ap_ids_ops(opts) do
|
||||
source_user = opts["muting_user"]
|
||||
source_user = opts[:muting_user]
|
||||
ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
|
||||
|
||||
ap_id_relationships =
|
||||
ap_id_relationships ++
|
||||
if opts["blocking_user"] && opts["blocking_user"] == source_user do
|
||||
[:block]
|
||||
if opts[:blocking_user] && opts[:blocking_user] == source_user do
|
||||
[:block | ap_id_relationships]
|
||||
else
|
||||
[]
|
||||
ap_id_relationships
|
||||
end
|
||||
|
||||
preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
|
||||
|
||||
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
|
||||
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
|
||||
restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
|
||||
restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts)
|
||||
|
||||
restrict_muted_reblogs_opts =
|
||||
Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
|
||||
Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts)
|
||||
|
||||
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
|
||||
end
|
||||
|
@ -1125,7 +1126,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> maybe_preload_report_notes(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> maybe_order(opts)
|
||||
|> restrict_recipients(recipients, opts["user"])
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_tag(opts)
|
||||
|> restrict_tag_reject(opts)
|
||||
|
@ -1145,19 +1146,21 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_pinned(opts)
|
||||
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|
||||
|> restrict_instance(opts)
|
||||
|> restrict_announce_object_actor(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_chat_messages(opts)
|
||||
|> exclude_invisible_actors(opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
||||
list_memberships = Pleroma.List.memberships(opts["user"])
|
||||
list_memberships = Pleroma.List.memberships(opts[:user])
|
||||
|
||||
fetch_activities_query(recipients ++ list_memberships, opts)
|
||||
|> Pagination.fetch_paginated(opts, pagination)
|
||||
|> Enum.reverse()
|
||||
|> maybe_update_cc(list_memberships, opts["user"])
|
||||
|> maybe_update_cc(list_memberships, opts[:user])
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -1170,19 +1173,17 @@ def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
|
|||
|> Activity.Queries.by_type("Like")
|
||||
|> Activity.with_joined_object()
|
||||
|> Object.with_joined_activity()
|
||||
|> select([_like, object, activity], %{activity | object: object})
|
||||
|> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
|
||||
|> order_by([like, _, _], desc_nulls_last: like.id)
|
||||
|> Pagination.fetch_paginated(
|
||||
Map.merge(params, %{"skip_order" => true}),
|
||||
pagination,
|
||||
:object_activity
|
||||
Map.merge(params, %{skip_order: true}),
|
||||
pagination
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
|
||||
when is_list(list_memberships) and length(list_memberships) > 0 do
|
||||
defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
|
||||
Enum.map(activities, fn
|
||||
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
|
||||
%{data: %{"bcc" => [_ | _] = bcc}} = activity ->
|
||||
if Enum.any?(bcc, &(&1 in list_memberships)) do
|
||||
update_in(activity.data["cc"], &[user_ap_id | &1])
|
||||
else
|
||||
|
@ -1196,7 +1197,7 @@ defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
|
|||
|
||||
defp maybe_update_cc(activities, _, _), do: activities
|
||||
|
||||
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
|
||||
defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do
|
||||
from(activity in query,
|
||||
where:
|
||||
fragment("? && ?", activity.recipients, ^recipients) or
|
||||
|
@ -1266,8 +1267,8 @@ defp object_to_user_data(data) do
|
|||
%{"type" => "Emoji"} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
|
||||
Map.put(acc, String.trim(name, ":"), url)
|
||||
|> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
|
||||
{String.trim(name, ":"), url}
|
||||
end)
|
||||
|
||||
locked = data["manuallyApprovesFollowers"] || false
|
||||
|
@ -1313,7 +1314,6 @@ defp object_to_user_data(data) do
|
|||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
user_data =
|
||||
if data["preferredUsername"] do
|
||||
Map.put(
|
||||
user_data,
|
||||
|
@ -1323,8 +1323,6 @@ defp object_to_user_data(data) do
|
|||
else
|
||||
Map.put(user_data, :nickname, nil)
|
||||
end
|
||||
|
||||
{:ok, user_data}
|
||||
end
|
||||
|
||||
def fetch_follow_information_for_user(user) do
|
||||
|
@ -1399,9 +1397,8 @@ defp collection_private(%{"first" => first}) do
|
|||
defp collection_private(_data), do: {:ok, true}
|
||||
|
||||
def user_data_from_user_object(data) do
|
||||
with {:ok, data} <- MRF.filter(data),
|
||||
{:ok, data} <- object_to_user_data(data) do
|
||||
{:ok, data}
|
||||
with {:ok, data} <- MRF.filter(data) do
|
||||
{:ok, object_to_user_data(data)}
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
|
@ -1409,15 +1406,14 @@ def user_data_from_user_object(data) do
|
|||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
{:ok, data} <- user_data_from_user_object(data),
|
||||
data <- maybe_update_follow_information(data) do
|
||||
{:ok, data}
|
||||
{:ok, data} <- user_data_from_user_object(data) do
|
||||
{:ok, maybe_update_follow_information(data)}
|
||||
else
|
||||
{:error, "Object has been deleted"} = e ->
|
||||
{:error, "Object has been deleted" = e} ->
|
||||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
{:error, e} ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
|
@ -1440,8 +1436,6 @@ def make_user_from_ap_id(ap_id) do
|
|||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1455,7 +1449,7 @@ def make_user_from_nickname(nickname) do
|
|||
end
|
||||
|
||||
# filter out broken threads
|
||||
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
|
||||
defp contain_broken_threads(%Activity{} = activity, %User{} = user) do
|
||||
entire_thread_visible_for_user?(activity, user)
|
||||
end
|
||||
|
||||
|
@ -1466,7 +1460,7 @@ def contain_activity(%Activity{} = activity, %User{} = user) do
|
|||
|
||||
def fetch_direct_messages_query do
|
||||
Activity
|
||||
|> restrict_type(%{"type" => "Create"})
|
||||
|> restrict_type(%{type: "Create"})
|
||||
|> restrict_visibility(%{visibility: "direct"})
|
||||
|> order_by([activity], asc: activity.id)
|
||||
end
|
||||
|
|
|
@ -238,6 +238,7 @@ def outbox(
|
|||
params
|
||||
|> Map.drop(["nickname", "page"])
|
||||
|> Map.put("include_poll_votes", true)
|
||||
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, for_user, params)
|
||||
|
||||
|
@ -354,6 +355,7 @@ def read_inbox(
|
|||
|> 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 =
|
||||
[user.ap_id | User.following(user)]
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
This module encodes our addressing policies and general shape of our objects.
|
||||
"""
|
||||
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
@ -65,6 +66,42 @@ def delete(actor, object_id) do
|
|||
}, []}
|
||||
end
|
||||
|
||||
def create(actor, object, recipients) do
|
||||
{:ok,
|
||||
%{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"actor" => actor.ap_id,
|
||||
"to" => recipients,
|
||||
"object" => object,
|
||||
"type" => "Create",
|
||||
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}, []}
|
||||
end
|
||||
|
||||
def chat_message(actor, recipient, content, opts \\ []) do
|
||||
basic = %{
|
||||
"id" => Utils.generate_object_id(),
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "ChatMessage",
|
||||
"to" => [recipient],
|
||||
"content" => content,
|
||||
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
"emoji" => Emoji.Formatter.get_emoji_map(content)
|
||||
}
|
||||
|
||||
case opts[:attachment] do
|
||||
%Object{data: attachment_data} ->
|
||||
{
|
||||
:ok,
|
||||
Map.put(basic, "attachment", attachment_data),
|
||||
[]
|
||||
}
|
||||
|
||||
_ ->
|
||||
{:ok, basic, []}
|
||||
end
|
||||
end
|
||||
|
||||
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def tombstone(actor, id) do
|
||||
{:ok,
|
||||
|
|
|
@ -8,11 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
def filter(policies, %{} = object) do
|
||||
policies
|
||||
|> Enum.reduce({:ok, object}, fn
|
||||
policy, {:ok, object} ->
|
||||
policy.filter(object)
|
||||
|
||||
_, error ->
|
||||
error
|
||||
policy, {:ok, object} -> policy.filter(object)
|
||||
_, error -> error
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
|
@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do
|
|||
|
||||
allow_list =
|
||||
Config.get(
|
||||
[:mrf_user_allowlist, String.to_atom(actor_info.host)],
|
||||
[:mrf_user_allowlist, actor_info.host],
|
||||
[]
|
||||
)
|
||||
|
||||
|
|
|
@ -9,13 +9,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
the system.
|
||||
"""
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
|
||||
|
||||
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||
|
@ -43,8 +45,20 @@ def validate(%{"type" => "Delete"} = object, meta) do
|
|||
|
||||
def validate(%{"type" => "Like"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object |> Map.from_struct())
|
||||
object
|
||||
|> LikeValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "ChatMessage"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> ChatMessageValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
@ -59,6 +73,18 @@ def validate(%{"type" => "EmojiReact"} = object, meta) do
|
|||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
|
||||
with {:ok, object_data} <- cast_and_apply(object),
|
||||
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
|
||||
{:ok, create_activity} <-
|
||||
create_activity
|
||||
|> CreateChatMessageValidator.cast_and_validate(meta)
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
create_activity = stringify_keys(create_activity)
|
||||
{:ok, create_activity, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Announce"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|
@ -69,19 +95,32 @@ def validate(%{"type" => "Announce"} = object, meta) do
|
|||
end
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "ChatMessage"} = object) do
|
||||
ChatMessageValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
|
||||
|
||||
def stringify_keys(%{__struct__: _} = object) do
|
||||
object
|
||||
|> Map.from_struct()
|
||||
|> stringify_keys
|
||||
end
|
||||
|
||||
def stringify_keys(object) do
|
||||
def stringify_keys(object) when is_map(object) do
|
||||
object
|
||||
|> Map.new(fn {key, val} -> {to_string(key), val} end)
|
||||
|> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
|
||||
end
|
||||
|
||||
def stringify_keys(object) when is_list(object) do
|
||||
object
|
||||
|> Enum.map(&stringify_keys/1)
|
||||
end
|
||||
|
||||
def stringify_keys(object), do: object
|
||||
|
||||
def fetch_actor(object) do
|
||||
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
|
||||
with {:ok, actor} <- ObjectValidators.ObjectID.cast(object["actor"]) do
|
||||
User.get_or_fetch_by_ap_id(actor)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
|
@ -19,14 +19,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:context, :string, autogenerate: {Utils, :generate_context_id, []})
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:cc, Types.Recipients, default: [])
|
||||
field(:published, Types.DateTime)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
|
|
|
@ -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
|
|
@ -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.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:type, :string)
|
||||
field(:content, ObjectValidators.SafeText)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:published, ObjectValidators.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
|
|
@ -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.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
alias Pleroma.Object
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:type, :string)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:object, ObjectValidators.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
|
|
@ -5,16 +5,16 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:type, :string)
|
||||
field(:to, {:array, :string})
|
||||
field(:cc, {:array, :string})
|
|
@ -6,8 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
|||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:cc, Types.Recipients, default: [])
|
||||
field(:deleted_activity_id, Types.ObjectID)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:deleted_activity_id, ObjectValidators.ObjectID)
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
|
@ -46,12 +46,13 @@ def add_deleted_activity_id(cng) do
|
|||
Answer
|
||||
Article
|
||||
Audio
|
||||
ChatMessage
|
||||
Event
|
||||
Note
|
||||
Page
|
||||
Question
|
||||
Video
|
||||
Tombstone
|
||||
Video
|
||||
}
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:context, :string)
|
||||
field(:content, :string)
|
||||
field(:to, {:array, :string}, default: [])
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
import Ecto.Changeset
|
||||
|
@ -15,13 +15,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:context, :string)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:cc, Types.Recipients, default: [])
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
|
@ -67,7 +67,7 @@ def fix_recipients(cng) do
|
|||
|
||||
with {[], []} <- {to, cc},
|
||||
%Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
|
||||
{:ok, actor} <- Types.ObjectID.cast(actor) do
|
||||
{:ok, actor} <- ObjectValidators.ObjectID.cast(actor) do
|
||||
cng
|
||||
|> put_change(:to, [actor])
|
||||
else
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:to, {:array, :string}, default: [])
|
||||
field(:cc, {:array, :string}, default: [])
|
||||
field(:bto, {:array, :string}, default: [])
|
||||
|
@ -22,10 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
|
|||
field(:type, :string)
|
||||
field(:content, :string)
|
||||
field(:context, :string)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:attributedTo, Types.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:attributedTo, ObjectValidators.ObjectID)
|
||||
field(:summary, :string)
|
||||
field(:published, Types.DateTime)
|
||||
field(:published, ObjectValidators.DateTime)
|
||||
# TODO: Write type
|
||||
field(:emoji, :map, default: %{})
|
||||
field(:sensitive, :boolean, default: false)
|
||||
|
@ -35,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
|
|||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inRepyTo, :string)
|
||||
field(:uri, Types.Uri)
|
||||
field(:uri, ObjectValidators.Uri)
|
||||
|
||||
field(:likes, {:array, :string}, default: [])
|
||||
field(:announcements, {:array, :string}, default: [])
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
|
||||
use Ecto.Type
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
|
||||
|
||||
def type, do: {:array, ObjectID}
|
||||
|
||||
def cast(object) when is_binary(object) do
|
||||
cast([object])
|
||||
end
|
||||
|
||||
def cast(data) when is_list(data) do
|
||||
data
|
||||
|> Enum.reduce({:ok, []}, fn element, acc ->
|
||||
case {acc, ObjectID.cast(element)} do
|
||||
{:error, _} -> :error
|
||||
{_, :error} -> :error
|
||||
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def cast(_) do
|
||||
:error
|
||||
end
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|
|||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:object, Types.ObjectID)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, {:array, :string}, default: [])
|
||||
field(:cc, {:array, :string}, default: [])
|
||||
end
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# 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.UrlObjectValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:type, :string)
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, :string)
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
end
|
|
@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
|
||||
def common_pipeline(object, meta) do
|
||||
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
|
||||
{:ok, {:ok, activity, meta}} ->
|
||||
SideEffects.handle_after_transaction(meta)
|
||||
{:ok, activity, meta}
|
||||
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
|
|
|
@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
collection, and so on.
|
||||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Streamer
|
||||
|
||||
def handle(object, meta \\ [])
|
||||
|
||||
|
@ -27,6 +32,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
|||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles
|
||||
# - Actually create object
|
||||
# - Rollback if we couldn't create it
|
||||
# - Set up notifications
|
||||
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
||||
with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, activity, meta}
|
||||
else
|
||||
e -> Repo.rollback(e)
|
||||
end
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Add announce to object
|
||||
# - Set up notification
|
||||
|
@ -88,6 +111,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
|
|||
Object.decrease_replies_count(in_reply_to)
|
||||
end
|
||||
|
||||
MessageReference.delete_for_object(deleted_object)
|
||||
|
||||
ActivityPub.stream_out(object)
|
||||
ActivityPub.stream_out_participations(deleted_object, user)
|
||||
:ok
|
||||
|
@ -112,6 +137,39 @@ def handle(object, meta) do
|
|||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
|
||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||
actor = User.get_cached_by_ap_id(object.data["actor"])
|
||||
recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
|
||||
|
||||
streamables =
|
||||
[[actor, recipient], [recipient, actor]]
|
||||
|> Enum.map(fn [user, other_user] ->
|
||||
if user.local do
|
||||
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
{:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
|
||||
|
||||
{
|
||||
["user", "user:pleroma_chat"],
|
||||
{user, %{cm_ref | chat: chat, object: object}}
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_streamables(streamables)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
# Nothing to do
|
||||
def handle_object_creation(object) do
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
|
||||
|
@ -148,4 +206,43 @@ def handle_undoing(
|
|||
end
|
||||
|
||||
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end)
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp send_streamables(meta) do
|
||||
Keyword.get(meta, :streamables, [])
|
||||
|> Enum.each(fn {topics, items} ->
|
||||
Streamer.stream(topics, items)
|
||||
end)
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp add_streamables(meta, streamables) do
|
||||
existing = Keyword.get(meta, :streamables, [])
|
||||
|
||||
meta
|
||||
|> Keyword.put(:streamables, streamables ++ existing)
|
||||
end
|
||||
|
||||
defp add_notifications(meta, notifications) do
|
||||
existing = Keyword.get(meta, :notifications, [])
|
||||
|
||||
meta
|
||||
|> Keyword.put(:notifications, notifications ++ existing)
|
||||
end
|
||||
|
||||
def handle_after_transaction(meta) do
|
||||
meta
|
||||
|> send_notifications()
|
||||
|> send_streamables()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,8 +8,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Repo
|
||||
|
@ -17,7 +19,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
@ -221,9 +222,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|
|||
|
||||
media_type =
|
||||
cond do
|
||||
is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
|
||||
is_binary(data["mediaType"]) -> data["mediaType"]
|
||||
is_binary(data["mimeType"]) -> data["mimeType"]
|
||||
is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
|
||||
MIME.valid?(data["mediaType"]) -> data["mediaType"]
|
||||
MIME.valid?(data["mimeType"]) -> data["mimeType"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
|
@ -527,7 +528,8 @@ def handle_incoming(
|
|||
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
|
||||
{:ok, %User{} = follower} <-
|
||||
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
|
||||
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity} <-
|
||||
ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
|
||||
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
|
||||
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
|
||||
{_, false} <- {:user_locked, User.locked?(followed)},
|
||||
|
@ -570,6 +572,7 @@ def handle_incoming(
|
|||
:noop
|
||||
end
|
||||
|
||||
ActivityPub.notify_and_stream(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
|
@ -590,6 +593,8 @@ def handle_incoming(
|
|||
User.update_follower_count(followed)
|
||||
User.update_following_count(follower)
|
||||
|
||||
Notification.update_notification_type(followed, follow_activity)
|
||||
|
||||
ActivityPub.accept(%{
|
||||
to: follow_activity.data["to"],
|
||||
type: "Accept",
|
||||
|
@ -657,6 +662,16 @@ def handle_incoming(
|
|||
|> handle_incoming(options)
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
|
||||
_options
|
||||
) do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{"type" => type} = data, _options)
|
||||
when type in ["Like", "EmojiReact", "Announce"] do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
|
@ -710,7 +725,7 @@ def handle_incoming(
|
|||
else
|
||||
{:error, {:validate_object, _}} = e ->
|
||||
# Check if we have a create activity for this
|
||||
with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
|
||||
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
|
||||
%Activity{data: %{"actor" => actor}} <-
|
||||
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
|
||||
# We have one, insert a tombstone and retry
|
||||
|
@ -1108,6 +1123,9 @@ def add_attributed_to(object) do
|
|||
Map.put(object, "attributedTo", attributed_to)
|
||||
end
|
||||
|
||||
# TODO: Revisit this
|
||||
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
|
||||
|
||||
def prepare_attachments(object) do
|
||||
attachments =
|
||||
object
|
||||
|
|
|
@ -245,7 +245,7 @@ defp lazy_put_object_defaults(activity, _), do: activity
|
|||
Inserts a full object if it is contained in an activity.
|
||||
"""
|
||||
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
|
||||
when is_map(object_data) and type in @supported_object_types do
|
||||
when type in @supported_object_types do
|
||||
with {:ok, object} <- Object.create(object_data) do
|
||||
map = Map.put(map, "object", object.data["id"])
|
||||
|
||||
|
@ -741,13 +741,12 @@ defp build_flag_object(_), do: []
|
|||
def get_reports(params, page, page_size) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", "Flag")
|
||||
|> Map.put("skip_preload", true)
|
||||
|> Map.put("preload_report_notes", true)
|
||||
|> Map.put("total", true)
|
||||
|> Map.put("limit", page_size)
|
||||
|> Map.put("offset", (page - 1) * page_size)
|
||||
|> Map.put(:type, "Flag")
|
||||
|> Map.put(:skip_preload, true)
|
||||
|> Map.put(:preload_report_notes, true)
|
||||
|> Map.put(:total, true)
|
||||
|> Map.put(:limit, page_size)
|
||||
|> Map.put(:offset, (page - 1) * page_size)
|
||||
|
||||
ActivityPub.fetch_activities([], params, :offset)
|
||||
end
|
||||
|
|
|
@ -16,7 +16,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.AdminAPI.ModerationLogView
|
||||
|
@ -59,7 +58,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:follows"], admin: true}
|
||||
when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
|
||||
when action in [:user_follow, :user_unfollow]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
@ -74,7 +73,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
when action in [
|
||||
:list_log,
|
||||
:stats,
|
||||
:relay_list,
|
||||
:need_reboot
|
||||
]
|
||||
)
|
||||
|
@ -228,10 +226,10 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
|
|||
|
||||
activities =
|
||||
ActivityPub.fetch_statuses(nil, %{
|
||||
"instance" => instance,
|
||||
"limit" => page_size,
|
||||
"offset" => (page - 1) * page_size,
|
||||
"exclude_reblogs" => !with_reblogs && "true"
|
||||
instance: instance,
|
||||
limit: page_size,
|
||||
offset: (page - 1) * page_size,
|
||||
exclude_reblogs: not with_reblogs
|
||||
})
|
||||
|
||||
conn
|
||||
|
@ -248,9 +246,9 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
|||
|
||||
activities =
|
||||
ActivityPub.fetch_user_activities(user, nil, %{
|
||||
"limit" => page_size,
|
||||
"godmode" => godmode,
|
||||
"exclude_reblogs" => !with_reblogs && "true"
|
||||
limit: page_size,
|
||||
godmode: godmode,
|
||||
exclude_reblogs: not with_reblogs
|
||||
})
|
||||
|
||||
conn
|
||||
|
@ -491,50 +489,6 @@ def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname"
|
|||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||
end
|
||||
|
||||
def relay_list(conn, _params) do
|
||||
with {:ok, list} <- Relay.list() do
|
||||
json(conn, %{relays: list})
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
end
|
||||
end
|
||||
|
||||
def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_follow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
|
||||
json(conn, target)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
|
||||
with {:ok, _message} <- Relay.unfollow(target) do
|
||||
ModerationLog.insert_log(%{
|
||||
action: "relay_unfollow",
|
||||
actor: admin,
|
||||
target: target
|
||||
})
|
||||
|
||||
json(conn, target)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Get a password reset token (base64 string) for given nickname"
|
||||
def get_password_reset(conn, %{"nickname" => nickname}) do
|
||||
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
|
||||
|
|
|
@ -33,7 +33,11 @@ def descriptions(conn, _params) do
|
|||
def show(conn, %{only_db: true}) do
|
||||
with :ok <- configurable_from_database() do
|
||||
configs = Pleroma.Repo.all(ConfigDB)
|
||||
render(conn, "index.json", %{configs: configs})
|
||||
|
||||
render(conn, "index.json", %{
|
||||
configs: configs,
|
||||
need_reboot: Restarter.Pleroma.need_reboot?()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -61,17 +65,20 @@ def show(conn, _params) do
|
|||
value
|
||||
end
|
||||
|
||||
%{
|
||||
group: ConfigDB.convert(group),
|
||||
key: ConfigDB.convert(key),
|
||||
value: ConfigDB.convert(merged_value)
|
||||
%ConfigDB{
|
||||
group: group,
|
||||
key: key,
|
||||
value: merged_value
|
||||
}
|
||||
|> Pleroma.Maps.put_if_present(:db, db)
|
||||
end)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|
||||
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
|
||||
render(conn, "index.json", %{
|
||||
configs: merged,
|
||||
need_reboot: Restarter.Pleroma.need_reboot?()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -91,24 +98,17 @@ def update(%{body_params: %{configs: configs}} = conn, _) do
|
|||
|
||||
{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
|
||||
|> Enum.map(fn {:ok, %{key: key, value: value} = config} ->
|
||||
Map.put(config, :db, ConfigDB.get_db_keys(value, key))
|
||||
end)
|
||||
|> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted))
|
||||
|
||||
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)
|
||||
|> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value))
|
||||
|
||||
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
|
||||
end
|
||||
|
|
67
lib/pleroma/web/admin_api/controllers/relay_controller.ex
Normal file
67
lib/pleroma/web/admin_api/controllers/relay_controller.ex
Normal 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
|
|
@ -29,11 +29,11 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
|
|||
def index(%{assigns: %{user: _admin}} = conn, params) do
|
||||
activities =
|
||||
ActivityPub.fetch_statuses(nil, %{
|
||||
"godmode" => params.godmode,
|
||||
"local_only" => params.local_only,
|
||||
"limit" => params.page_size,
|
||||
"offset" => (params.page - 1) * params.page_size,
|
||||
"exclude_reblogs" => not params.with_reblogs
|
||||
godmode: params.godmode,
|
||||
local_only: params.local_only,
|
||||
limit: params.page_size,
|
||||
offset: (params.page - 1) * params.page_size,
|
||||
exclude_reblogs: not params.with_reblogs
|
||||
})
|
||||
|
||||
render(conn, "index.json", activities: activities, as: :activity)
|
||||
|
|
|
@ -76,7 +76,8 @@ def render("show.json", %{user: user}) do
|
|||
"local" => user.local,
|
||||
"roles" => User.roles(user),
|
||||
"tags" => user.tags || [],
|
||||
"confirmation_pending" => user.confirmation_pending
|
||||
"confirmation_pending" => user.confirmation_pending,
|
||||
"url" => user.uri || user.ap_id
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -5,23 +5,20 @@
|
|||
defmodule Pleroma.Web.AdminAPI.ConfigView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
def render("index.json", %{configs: configs} = params) do
|
||||
map = %{
|
||||
configs: render_many(configs, __MODULE__, "show.json", as: :config)
|
||||
}
|
||||
alias Pleroma.ConfigDB
|
||||
|
||||
if params[:need_reboot] do
|
||||
Map.put(map, :need_reboot, true)
|
||||
else
|
||||
map
|
||||
end
|
||||
def render("index.json", %{configs: configs} = params) do
|
||||
%{
|
||||
configs: render_many(configs, __MODULE__, "show.json", as: :config),
|
||||
need_reboot: params[:need_reboot]
|
||||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{config: config}) do
|
||||
map = %{
|
||||
key: config.key,
|
||||
group: config.group,
|
||||
value: Pleroma.ConfigDB.from_binary_with_convert(config.value)
|
||||
key: ConfigDB.to_json_types(config.key),
|
||||
group: ConfigDB.to_json_types(config.group),
|
||||
value: ConfigDB.to_json_types(config.value)
|
||||
}
|
||||
|
||||
if config.db != [] do
|
||||
|
|
|
@ -18,7 +18,7 @@ def render("index.json", %{reports: reports}) do
|
|||
%{
|
||||
reports:
|
||||
reports[:items]
|
||||
|> Enum.map(&Report.extract_report_info(&1))
|
||||
|> Enum.map(&Report.extract_report_info/1)
|
||||
|> Enum.map(&render(__MODULE__, "show.json", &1))
|
||||
|> Enum.reverse(),
|
||||
total: reports[:total]
|
||||
|
|
83
lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
Normal file
83
lib/pleroma/web/api_spec/operations/admin/relay_operation.ex
Normal 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
|
355
lib/pleroma/web/api_spec/operations/chat_operation.ex
Normal file
355
lib/pleroma/web/api_spec/operations/chat_operation.ex
Normal 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
|
|
@ -183,8 +183,8 @@ defp notification_type do
|
|||
"favourite",
|
||||
"reblog",
|
||||
"mention",
|
||||
"poll",
|
||||
"pleroma:emoji_reaction",
|
||||
"pleroma:chat_mention",
|
||||
"move",
|
||||
"follow_request"
|
||||
],
|
||||
|
|
|
@ -333,7 +333,8 @@ def favourites_operation do
|
|||
%Operation{
|
||||
tags: ["Statuses"],
|
||||
summary: "Favourited statuses",
|
||||
description: "Statuses the user has favourited",
|
||||
description:
|
||||
"Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
|
||||
operationId: "StatusController.favourites",
|
||||
parameters: pagination_params(),
|
||||
security: [%{"oAuth" => ["read:favourites"]}],
|
||||
|
|
|
@ -141,6 +141,11 @@ defp create_request do
|
|||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive poll notifications?"
|
||||
},
|
||||
"pleroma:chat_mention": %Schema{
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive chat notifications?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
75
lib/pleroma/web/api_spec/schemas/chat.ex
Normal file
75
lib/pleroma/web/api_spec/schemas/chat.ex
Normal 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
|
41
lib/pleroma/web/api_spec/schemas/chat_message.ex
Normal file
41
lib/pleroma/web/api_spec/schemas/chat_message.ex
Normal 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
|
|
@ -197,6 +197,13 @@ defp preview?(draft) do
|
|||
|
||||
defp changes(draft) do
|
||||
direct? = draft.visibility == "direct"
|
||||
additional = %{"cc" => draft.cc, "directMessage" => direct?}
|
||||
|
||||
additional =
|
||||
case draft.expires_at do
|
||||
%NaiveDateTime{} = expires_at -> Map.put(additional, "expires_at", expires_at)
|
||||
_ -> additional
|
||||
end
|
||||
|
||||
changes =
|
||||
%{
|
||||
|
@ -204,7 +211,7 @@ defp changes(draft) do
|
|||
actor: draft.user,
|
||||
context: draft.context,
|
||||
object: draft.object,
|
||||
additional: %{"cc" => draft.cc, "directMessage" => direct?}
|
||||
additional: additional
|
||||
}
|
||||
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
|
@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
||||
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
||||
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
||||
{_, {:ok, chat_message_data, _meta}} <-
|
||||
{:build_object,
|
||||
Builder.chat_message(
|
||||
user,
|
||||
recipient.ap_id,
|
||||
content |> format_chat_content,
|
||||
attachment: maybe_attachment
|
||||
)},
|
||||
{_, {:ok, create_activity_data, _meta}} <-
|
||||
{:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
Pipeline.common_pipeline(create_activity_data,
|
||||
local: true
|
||||
)} do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_chat_content(nil), do: nil
|
||||
|
||||
defp format_chat_content(content) do
|
||||
{text, _, _} =
|
||||
content
|
||||
|> Formatter.html_escape("text/plain")
|
||||
|> Formatter.linkify()
|
||||
|> (fn {text, mentions, tags} ->
|
||||
{String.replace(text, ~r/\r?\n/, "<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
|
||||
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
|
||||
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
|
||||
|
@ -73,6 +121,7 @@ def accept_follow_request(follower, followed) do
|
|||
object: follow_activity.data["id"],
|
||||
type: "Accept"
|
||||
}) do
|
||||
Notification.update_notification_type(followed, follow_activity)
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
@ -374,20 +423,10 @@ def listen(user, data) do
|
|||
|
||||
def post(user, %{status: _} = data) do
|
||||
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
|
||||
draft.changes
|
||||
|> ActivityPub.create(draft.preview?)
|
||||
|> maybe_create_activity_expiration(draft.expires_at)
|
||||
ActivityPub.create(draft.changes, draft.preview?)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
|
||||
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_create_activity_expiration(result, _), do: result
|
||||
|
||||
def pin(id, %{ap_id: user_ap_id} = user) do
|
||||
with %Activity{
|
||||
actor: ^user_ap_id,
|
||||
|
@ -427,12 +466,13 @@ def remove_mute(user, activity) do
|
|||
{:ok, activity}
|
||||
end
|
||||
|
||||
def thread_muted?(%{id: nil} = _user, _activity), do: false
|
||||
|
||||
def thread_muted?(user, activity) do
|
||||
ThreadMute.exists?(user.id, activity.data["context"])
|
||||
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
|
||||
when is_binary("context") do
|
||||
ThreadMute.exists?(user_id, context)
|
||||
end
|
||||
|
||||
def thread_muted?(_, _), do: false
|
||||
|
||||
def report(user, data) do
|
||||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
|
|
|
@ -429,7 +429,7 @@ def maybe_notify_mentioned_recipients(
|
|||
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
|
||||
)
|
||||
when type == "Create" do
|
||||
object = Object.normalize(activity)
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
object_data =
|
||||
cond do
|
||||
|
|
|
@ -57,35 +57,36 @@ def add_link_headers(conn, activities, extra_params) do
|
|||
end
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, activities, extra_params \\ %{}) do
|
||||
case List.last(activities) do
|
||||
%{id: max_id} ->
|
||||
@id_keys Pagination.page_keys() -- ["limit", "order"]
|
||||
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
|
||||
params =
|
||||
conn.params
|
||||
|> Map.drop(Map.keys(conn.path_params))
|
||||
|> Map.merge(extra_params)
|
||||
|> Map.drop(Pagination.page_keys() -- ["limit", "order"])
|
||||
|> Map.drop(@id_keys)
|
||||
|
||||
min_id =
|
||||
%{
|
||||
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
|
||||
"prev" => current_url(conn, Map.put(params, :min_id, min_id)),
|
||||
"id" => current_url(conn)
|
||||
}
|
||||
end
|
||||
|
||||
def get_pagination_fields(conn, activities, extra_params \\ %{}) do
|
||||
case List.last(activities) do
|
||||
%{pagination_id: max_id} when not is_nil(max_id) ->
|
||||
%{pagination_id: 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))
|
||||
}
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
|
||||
# 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
|
||||
%{id: max_id} ->
|
||||
%{id: min_id} =
|
||||
activities
|
||||
|> List.first()
|
||||
|
||||
build_pagination_fields(conn, min_id, max_id, extra_params)
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
|
@ -93,8 +94,7 @@ def get_pagination_fields(conn, activities, extra_params \\ %{}) do
|
|||
end
|
||||
|
||||
def assign_account_by_id(conn, _) do
|
||||
# TODO: use `conn.params[:id]` only after moving to OpenAPI
|
||||
case Pleroma.User.get_cached_by_id(conn.params[:id] || conn.params["id"]) do
|
||||
case Pleroma.User.get_cached_by_id(conn.params.id) do
|
||||
%Pleroma.User{} = account -> assign(conn, :account, account)
|
||||
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
|
||||
end
|
||||
|
|
|
@ -13,8 +13,8 @@ def feed(conn, %{"tag" => raw_tag} = params) do
|
|||
{format, tag} = parse_tag(raw_tag)
|
||||
|
||||
activities =
|
||||
%{"type" => ["Create"], "tag" => tag}
|
||||
|> Pleroma.Maps.put_if_present("max_id", params["max_id"])
|
||||
%{type: ["Create"], tag: tag}
|
||||
|> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|
||||
conn
|
||||
|
|
|
@ -50,10 +50,10 @@ def feed(conn, %{"nickname" => nickname} = params) do
|
|||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
|
||||
activities =
|
||||
%{
|
||||
"type" => ["Create"],
|
||||
"actor_id" => user.ap_id
|
||||
type: ["Create"],
|
||||
actor_id: user.ap_id
|
||||
}
|
||||
|> Pleroma.Maps.put_if_present("max_id", params["max_id"])
|
||||
|> Pleroma.Maps.put_if_present(:max_id, params["max_id"])
|
||||
|> ActivityPub.fetch_public_or_unlisted_activities()
|
||||
|
||||
conn
|
||||
|
|
|
@ -165,6 +165,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|
|||
end)
|
||||
|> Maps.put_if_present(:name, params[:display_name])
|
||||
|> Maps.put_if_present(:bio, params[:note])
|
||||
|> Maps.put_if_present(:raw_bio, params[:note])
|
||||
|> Maps.put_if_present(:avatar, params[:avatar])
|
||||
|> Maps.put_if_present(:banner, params[:header])
|
||||
|> Maps.put_if_present(:background, params[:pleroma_background_image])
|
||||
|
@ -244,9 +245,7 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
|||
params =
|
||||
params
|
||||
|> Map.delete(:tagged)
|
||||
|> Enum.filter(&(not is_nil(&1)))
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("tag", params[:tagged])
|
||||
|> Map.put(:tag, params[:tagged])
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do
|
|||
|
||||
@doc "GET /api/v1/conversations"
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
params = stringify_pagination_params(params)
|
||||
participations = Participation.for_user_with_last_activity_id(user, params)
|
||||
|
||||
conn
|
||||
|
@ -37,20 +36,4 @@ def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
|
|||
render(conn, "participation.json", participation: participation, for: user)
|
||||
end
|
||||
end
|
||||
|
||||
defp stringify_pagination_params(params) do
|
||||
atom_keys =
|
||||
Pleroma.Pagination.page_keys()
|
||||
|> Enum.map(&String.to_atom(&1))
|
||||
|
||||
str_keys =
|
||||
params
|
||||
|> Map.take(atom_keys)
|
||||
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
params
|
||||
|> Map.delete(atom_keys)
|
||||
|> Map.merge(str_keys)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,8 +42,20 @@ def index(conn, %{account_id: account_id} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
@default_notification_types ~w{
|
||||
mention
|
||||
follow
|
||||
follow_request
|
||||
reblog
|
||||
favourite
|
||||
move
|
||||
pleroma:emoji_reaction
|
||||
}
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||
params =
|
||||
Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||
|> Map.put_new("include_types", @default_notification_types)
|
||||
|
||||
notifications = MastodonAPI.get_notifications(user, params)
|
||||
|
||||
conn
|
||||
|
|
|
@ -124,6 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do
|
|||
defp prepare_tags(query, add_joined_tag \\ true) do
|
||||
tags =
|
||||
query
|
||||
|> preprocess_uri_query()
|
||||
|> String.split(~r/[^#\w]+/u, trim: true)
|
||||
|> Enum.uniq_by(&String.downcase/1)
|
||||
|
||||
|
@ -147,6 +148,20 @@ defp prepare_tags(query, add_joined_tag \\ true) do
|
|||
end
|
||||
end
|
||||
|
||||
# If `query` is a URI, returns last component of its path, otherwise returns `query`
|
||||
defp preprocess_uri_query(query) do
|
||||
if query =~ ~r/https?:\/\// do
|
||||
query
|
||||
|> String.trim_trailing("/")
|
||||
|> URI.parse()
|
||||
|> Map.get(:path)
|
||||
|> String.split("/")
|
||||
|> Enum.at(-1)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp joined_tag(tags) do
|
||||
tags
|
||||
|> Enum.map(fn tag -> String.capitalize(tag) end)
|
||||
|
|
|
@ -359,9 +359,9 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
|
|||
with %Activity{} = activity <- Activity.get_by_id(id) do
|
||||
activities =
|
||||
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
"exclude_id" => activity.id
|
||||
blocking_user: user,
|
||||
user: user,
|
||||
exclude_id: activity.id
|
||||
})
|
||||
|
||||
render(conn, "context.json", activity: activity, activities: activities, user: user)
|
||||
|
@ -370,11 +370,6 @@ def context(%{assigns: %{user: user}} = conn, %{id: id}) do
|
|||
|
||||
@doc "GET /api/v1/favourites"
|
||||
def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.take(Pleroma.Pagination.page_keys())
|
||||
|
||||
activities = ActivityPub.fetch_favourites(user, params)
|
||||
|
||||
conn
|
||||
|
|
|
@ -44,12 +44,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
def home(%{assigns: %{user: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> Map.put(:announce_filtering_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|
||||
activities =
|
||||
[user.ap_id | User.following(user)]
|
||||
|
@ -69,10 +69,9 @@ def home(%{assigns: %{user: user}} = conn, params) do
|
|||
def direct(%{assigns: %{user: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put(:type, "Create")
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:visibility, "direct")
|
||||
|
||||
activities =
|
||||
|
@ -91,9 +90,7 @@ def direct(%{assigns: %{user: user}} = conn, params) do
|
|||
|
||||
# GET /api/v1/timelines/public
|
||||
def public(%{assigns: %{user: user}} = conn, params) do
|
||||
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
|
||||
|
||||
local_only = params["local"]
|
||||
local_only = params[:local]
|
||||
|
||||
cfg_key =
|
||||
if local_only do
|
||||
|
@ -109,11 +106,11 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
|||
else
|
||||
activities =
|
||||
params
|
||||
|> Map.put("type", ["Create"])
|
||||
|> Map.put("local_only", local_only)
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("reply_filtering_user", user)
|
||||
|> Map.put(:type, ["Create"])
|
||||
|> Map.put(:local_only, local_only)
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:reply_filtering_user, user)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|
||||
conn
|
||||
|
@ -128,39 +125,38 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
|||
|
||||
defp hashtag_fetching(params, user, local_only) do
|
||||
tags =
|
||||
[params["tag"], params["any"]]
|
||||
[params[:tag], params[:any]]
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.map(&String.downcase/1)
|
||||
|
||||
tag_all =
|
||||
params
|
||||
|> Map.get("all", [])
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
|> Map.get(:all, [])
|
||||
|> Enum.map(&String.downcase/1)
|
||||
|
||||
tag_reject =
|
||||
params
|
||||
|> Map.get("none", [])
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
|> Map.get(:none, [])
|
||||
|> Enum.map(&String.downcase/1)
|
||||
|
||||
_activities =
|
||||
params
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("local_only", local_only)
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put("tag", tags)
|
||||
|> Map.put("tag_all", tag_all)
|
||||
|> Map.put("tag_reject", tag_reject)
|
||||
|> Map.put(:type, "Create")
|
||||
|> Map.put(:local_only, local_only)
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|> Map.put(:tag, tags)
|
||||
|> Map.put(:tag_all, tag_all)
|
||||
|> Map.put(:tag_reject, tag_reject)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
end
|
||||
|
||||
# GET /api/v1/timelines/tag/:tag
|
||||
def hashtag(%{assigns: %{user: user}} = conn, params) do
|
||||
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
|
||||
local_only = params["local"]
|
||||
local_only = params[:local]
|
||||
activities = hashtag_fetching(params, user, local_only)
|
||||
|
||||
conn
|
||||
|
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.ScheduledActivity
|
||||
|
@ -82,15 +81,11 @@ defp cast_params(params) do
|
|||
end
|
||||
|
||||
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
|
||||
ap_types = convert_and_filter_mastodon_types(mastodon_types)
|
||||
|
||||
where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
where(query, [n], n.type in ^mastodon_types)
|
||||
end
|
||||
|
||||
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
|
||||
ap_types = convert_and_filter_mastodon_types(mastodon_types)
|
||||
|
||||
where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
where(query, [n], n.type not in ^mastodon_types)
|
||||
end
|
||||
|
||||
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
|
||||
|
@ -98,10 +93,4 @@ defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
|
|||
end
|
||||
|
||||
defp restrict(query, _, _), do: query
|
||||
|
||||
defp convert_and_filter_mastodon_types(types) do
|
||||
types
|
||||
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -224,7 +224,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
fields: user.fields,
|
||||
bot: bot,
|
||||
source: %{
|
||||
note: prepare_user_bio(user),
|
||||
note: user.raw_bio || "",
|
||||
sensitive: false,
|
||||
fields: user.raw_fields,
|
||||
pleroma: %{
|
||||
|
@ -235,6 +235,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|
||||
# Pleroma extension
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
confirmation_pending: user.confirmation_pending,
|
||||
tags: user.tags,
|
||||
hide_followers_count: user.hide_followers_count,
|
||||
|
@ -259,17 +260,6 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|> maybe_put_unread_notification_count(user, opts[:for])
|
||||
end
|
||||
|
||||
defp prepare_user_bio(%User{bio: ""}), do: ""
|
||||
|
||||
defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
|
||||
bio
|
||||
|> String.replace(~r(<br */?>), "\n")
|
||||
|> Pleroma.HTML.strip_tags()
|
||||
|> HtmlEntities.decode()
|
||||
end
|
||||
|
||||
defp prepare_user_bio(_), do: ""
|
||||
|
||||
defp username_from_nickname(string) when is_binary(string) do
|
||||
hd(String.split(string, "@"))
|
||||
end
|
||||
|
|
|
@ -23,10 +23,13 @@ def render("participation.json", %{participation: participation, for: user}) do
|
|||
|
||||
last_activity_id =
|
||||
with nil <- participation.last_activity_id do
|
||||
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
|
||||
"user" => user,
|
||||
"blocking_user" => user
|
||||
})
|
||||
ActivityPub.fetch_latest_direct_activity_id_for_context(
|
||||
participation.conversation.ap_id,
|
||||
%{
|
||||
user: user,
|
||||
blocking_user: user
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
activity = Activity.get_by_id_with_object(last_activity_id)
|
||||
|
|
|
@ -69,7 +69,8 @@ def features do
|
|||
if Config.get([:instance, :safe_dm_mentions]) do
|
||||
"safe_dm_mentions"
|
||||
end,
|
||||
"pleroma_emoji_reactions"
|
||||
"pleroma_emoji_reactions",
|
||||
"pleroma_chat_messages"
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
|
|
@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
@parent_types ~w{Like Announce EmojiReact}
|
||||
|
||||
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
|
||||
activities = Enum.map(notifications, & &1.activity)
|
||||
|
||||
parent_activities =
|
||||
activities
|
||||
|> Enum.filter(
|
||||
&(Activity.mastodon_notification_type(&1) in [
|
||||
"favourite",
|
||||
"reblog",
|
||||
"pleroma:emoji_reaction"
|
||||
])
|
||||
)
|
||||
|> Enum.filter(fn
|
||||
%{data: %{"type" => type}} ->
|
||||
type in @parent_types
|
||||
end)
|
||||
|> Enum.map(& &1.data["object"])
|
||||
|> Activity.create_by_object_ap_id()
|
||||
|> Activity.with_preloaded_object(:left)
|
||||
|
@ -42,8 +44,9 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
|
|||
true ->
|
||||
move_activities_targets =
|
||||
activities
|
||||
|> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
|
||||
|> Enum.filter(&(&1.data["type"] == "Move"))
|
||||
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
actors =
|
||||
activities
|
||||
|
@ -79,19 +82,18 @@ def render(
|
|||
end
|
||||
end
|
||||
|
||||
mastodon_type = Activity.mastodon_notification_type(activity)
|
||||
|
||||
# Note: :relationships contain user mutes (needed for :muted flag in :status)
|
||||
status_render_opts = %{relationships: opts[:relationships]}
|
||||
|
||||
with %{id: _} = account <-
|
||||
account =
|
||||
AccountView.render(
|
||||
"show.json",
|
||||
%{user: actor, for: reading_user}
|
||||
) do
|
||||
)
|
||||
|
||||
response = %{
|
||||
id: to_string(notification.id),
|
||||
type: mastodon_type,
|
||||
type: notification.type,
|
||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||
account: account,
|
||||
pleroma: %{
|
||||
|
@ -99,7 +101,7 @@ def render(
|
|||
}
|
||||
}
|
||||
|
||||
case mastodon_type do
|
||||
case notification.type do
|
||||
"mention" ->
|
||||
put_status(response, activity, reading_user, status_render_opts)
|
||||
|
||||
|
@ -117,14 +119,11 @@ def render(
|
|||
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|
||||
|> put_emoji(activity)
|
||||
|
||||
"pleroma:chat_mention" ->
|
||||
put_chat_message(response, activity, reading_user, status_render_opts)
|
||||
|
||||
type when type in ["follow", "follow_request"] ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -132,6 +131,17 @@ defp put_emoji(response, activity) do
|
|||
Map.put(response, :emoji, activity.data["content"])
|
||||
end
|
||||
|
||||
defp put_chat_message(response, activity, reading_user, opts) do
|
||||
object = Object.normalize(activity)
|
||||
author = User.get_cached_by_ap_id(object.data["actor"])
|
||||
chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
|
||||
chat_message_render = MessageReferenceView.render("show.json", render_opts)
|
||||
|
||||
Map.put(response, :chat_message, chat_message_render)
|
||||
end
|
||||
|
||||
defp put_status(response, activity, reading_user, opts) do
|
||||
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
|
||||
status_render = StatusView.render("show.json", status_render_opts)
|
||||
|
|
|
@ -377,8 +377,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
|||
page_url_data = URI.parse(page_url)
|
||||
|
||||
page_url_data =
|
||||
if rich_media[:url] != nil do
|
||||
URI.merge(page_url_data, URI.parse(rich_media[:url]))
|
||||
if is_binary(rich_media["url"]) do
|
||||
URI.merge(page_url_data, URI.parse(rich_media["url"]))
|
||||
else
|
||||
page_url_data
|
||||
end
|
||||
|
@ -386,11 +386,9 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
|||
page_url = page_url_data |> to_string
|
||||
|
||||
image_url =
|
||||
if rich_media[:image] != nil do
|
||||
URI.merge(page_url_data, URI.parse(rich_media[:image]))
|
||||
if is_binary(rich_media["image"]) do
|
||||
URI.merge(page_url_data, URI.parse(rich_media["image"]))
|
||||
|> to_string
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%{
|
||||
|
@ -399,8 +397,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
|||
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
|
||||
url: page_url,
|
||||
image: image_url |> MediaProxy.url(),
|
||||
title: rich_media[:title] || "",
|
||||
description: rich_media[:description] || "",
|
||||
title: rich_media["title"] || "",
|
||||
description: rich_media["description"] || "",
|
||||
pleroma: %{
|
||||
opengraph: rich_media
|
||||
}
|
||||
|
|
|
@ -126,10 +126,9 @@ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params)
|
|||
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("favorited_by", user.ap_id)
|
||||
|> Map.put("blocking_user", for_user)
|
||||
|> Map.put(:type, "Create")
|
||||
|> Map.put(:favorited_by, user.ap_id)
|
||||
|> Map.put(:blocking_user, for_user)
|
||||
|
||||
recipients =
|
||||
if for_user do
|
||||
|
|
174
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
Normal file
174
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
Normal 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
|
|
@ -42,15 +42,14 @@ def statuses(
|
|||
Participation.get(participation_id, preload: [:conversation]) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|
||||
activities =
|
||||
participation.conversation.ap_id
|
||||
|> ActivityPub.fetch_activities_for_context_query(params)
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false))
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|
|
|
@ -36,10 +36,7 @@ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
|||
|
||||
def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", ["Listen"])
|
||||
params = Map.put(params, :type, ["Listen"])
|
||||
|
||||
activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue