Merge branch 'develop' into refactor/notification_settings
This commit is contained in:
commit
433c01b370
304 changed files with 14459 additions and 5927 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
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -6,18 +6,36 @@ 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`
|
||||
- Using the `only_media` filter on timelines will now exclude reblog media
|
||||
- MFR policy to set global expiration for all local Create activities
|
||||
- OGP rich media parser merged with TwitterCard
|
||||
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
|
||||
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- **Breaking:** Emoji API: changed methods and renamed routes.
|
||||
- **Breaking:** Notification Settings API for suppressing notification
|
||||
now supports the following controls: `from_followers`, `from_following`,
|
||||
and `from_strangers`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Admin API Changes</summary>
|
||||
|
||||
- Status visibility stats: now can return stats per instance.
|
||||
|
||||
- Mix task to refresh counter cache (`mix pleroma.refresh_counter_cache`)
|
||||
</details>
|
||||
|
||||
### Removed
|
||||
- **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.
|
||||
|
@ -28,17 +46,22 @@ 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
|
||||
- Support pagination in emoji packs API (for packs and for files in pack)
|
||||
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
- Mastodon API: Extended `/api/v1/instance`.
|
||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
||||
- Mastodon API: Add support for filtering replies in public and home timelines
|
||||
- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`
|
||||
- Admin API: endpoints for create/update/delete OAuth Apps.
|
||||
- Admin API: endpoint for status view.
|
||||
- OTP: Add command to reload emoji packs
|
||||
</details>
|
||||
|
||||
### Fixed
|
||||
|
@ -47,6 +70,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Fix follower/blocks import when nicknames starts with @
|
||||
- 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)]
|
||||
|
||||
|
@ -85,6 +110,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
2. Run database migrations (inside Pleroma directory):
|
||||
- OTP: `./bin/pleroma_ctl migrate`
|
||||
- From Source: `mix ecto.migrate`
|
||||
3. Reset status visibility counters (inside Pleroma directory):
|
||||
- OTP: `./bin/pleroma_ctl refresh_counter_cache`
|
||||
- From Source: `mix pleroma.refresh_counter_cache`
|
||||
|
||||
|
||||
## [2.0.2] - 2020-04-08
|
||||
|
|
10
README.md
10
README.md
|
@ -34,6 +34,16 @@ Currently Pleroma is not packaged by any OS/Distros, but if you want to package
|
|||
### Docker
|
||||
While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
|
||||
|
||||
### Compilation Troubleshooting
|
||||
If you ever encounter compilation issues during the updating of Pleroma, you can try these commands and see if they fix things:
|
||||
|
||||
- `mix deps.clean --all`
|
||||
- `mix local.rebar`
|
||||
- `mix local.hex`
|
||||
- `rm -r _build`
|
||||
|
||||
If you are not developing Pleroma, it is better to use the OTP release, which comes with everything precompiled.
|
||||
|
||||
## Documentation
|
||||
- Latest Released revision: <https://docs.pleroma.social>
|
||||
- Latest Git revision: <https://docs-develop.pleroma.social>
|
||||
|
|
|
@ -22,8 +22,21 @@ defmodule Pleroma.LoadTesting.Activities do
|
|||
@max_concurrency 10
|
||||
|
||||
@visibility ~w(public private direct unlisted)
|
||||
@types ~w(simple emoji mentions hell_thread attachment tag like reblog simple_thread remote)
|
||||
@groups ~w(user friends non_friends)
|
||||
@types [
|
||||
:simple,
|
||||
:emoji,
|
||||
:mentions,
|
||||
:hell_thread,
|
||||
:attachment,
|
||||
:tag,
|
||||
:like,
|
||||
:reblog,
|
||||
:simple_thread
|
||||
]
|
||||
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
|
||||
@remote_groups [:friends_remote, :non_friends_remote]
|
||||
@friends_groups [:friends_local, :friends_remote]
|
||||
@non_friends_groups [:non_friends_local, :non_friends_remote]
|
||||
|
||||
@spec generate(User.t(), keyword()) :: :ok
|
||||
def generate(user, opts \\ []) do
|
||||
|
@ -34,33 +47,24 @@ def generate(user, opts \\ []) do
|
|||
|
||||
opts = Keyword.merge(@defaults, opts)
|
||||
|
||||
friends =
|
||||
user
|
||||
|> Users.get_users(limit: opts[:friends_used], local: :local, friends?: true)
|
||||
|> Enum.shuffle()
|
||||
users = Users.prepare_users(user, opts)
|
||||
|
||||
non_friends =
|
||||
user
|
||||
|> Users.get_users(limit: opts[:non_friends_used], local: :local, friends?: false)
|
||||
|> Enum.shuffle()
|
||||
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
|
||||
|
||||
task_data =
|
||||
for visibility <- @visibility,
|
||||
type <- @types,
|
||||
group <- @groups,
|
||||
group <- [:user | @groups],
|
||||
do: {visibility, type, group}
|
||||
|
||||
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
|
||||
|
||||
friends_thread = Enum.take(friends, 5)
|
||||
non_friends_thread = Enum.take(friends, 5)
|
||||
|
||||
public_long_thread = fn ->
|
||||
generate_long_thread("public", user, friends_thread, non_friends_thread, opts)
|
||||
generate_long_thread("public", users, opts)
|
||||
end
|
||||
|
||||
private_long_thread = fn ->
|
||||
generate_long_thread("private", user, friends_thread, non_friends_thread, opts)
|
||||
generate_long_thread("private", users, opts)
|
||||
end
|
||||
|
||||
iterations = opts[:iterations]
|
||||
|
@ -73,10 +77,10 @@ def generate(user, opts \\ []) do
|
|||
i when i == iterations - 2 ->
|
||||
spawn(public_long_thread)
|
||||
spawn(private_long_thread)
|
||||
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
|
||||
generate_activities(users, Enum.shuffle(task_data), opts)
|
||||
|
||||
_ ->
|
||||
generate_activities(user, friends, non_friends, Enum.shuffle(task_data), opts)
|
||||
generate_activities(users, Enum.shuffle(task_data), opts)
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
@ -127,16 +131,16 @@ def generate_tagged_activities(opts \\ []) do
|
|||
end)
|
||||
end
|
||||
|
||||
defp generate_long_thread(visibility, user, friends, non_friends, _opts) do
|
||||
defp generate_long_thread(visibility, users, _opts) do
|
||||
group =
|
||||
if visibility == "public",
|
||||
do: "friends",
|
||||
else: "user"
|
||||
do: :friends_local,
|
||||
else: :user
|
||||
|
||||
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
CommonAPI.post(users[:user], %{
|
||||
status: "Start of #{visibility} long thread",
|
||||
visibility: visibility
|
||||
})
|
||||
|
@ -150,31 +154,28 @@ defp generate_long_thread(visibility, user, friends, non_friends, _opts) do
|
|||
Map.put(state, key, activity)
|
||||
end)
|
||||
|
||||
acc = {activity.id, ["@" <> user.nickname, "reply to long thread"]}
|
||||
insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc)
|
||||
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
|
||||
insert_replies_for_long_thread(tasks, visibility, users, acc)
|
||||
IO.puts("Generating #{visibility} long thread ended\n")
|
||||
end
|
||||
|
||||
defp insert_replies_for_long_thread(tasks, visibility, user, friends, non_friends, acc) do
|
||||
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
|
||||
Enum.reduce(tasks, acc, fn
|
||||
"friend", {id, data} ->
|
||||
friend = Enum.random(friends)
|
||||
insert_reply(friend, List.delete(data, "@" <> friend.nickname), id, visibility)
|
||||
|
||||
"non_friend", {id, data} ->
|
||||
non_friend = Enum.random(non_friends)
|
||||
insert_reply(non_friend, List.delete(data, "@" <> non_friend.nickname), id, visibility)
|
||||
|
||||
"user", {id, data} ->
|
||||
:user, {id, data} ->
|
||||
user = users[:user]
|
||||
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
|
||||
|
||||
group, {id, data} ->
|
||||
replier = Enum.random(users[group])
|
||||
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
|
||||
end)
|
||||
end
|
||||
|
||||
defp generate_activities(user, friends, non_friends, task_data, opts) do
|
||||
defp generate_activities(users, task_data, opts) do
|
||||
Task.async_stream(
|
||||
task_data,
|
||||
fn {visibility, type, group} ->
|
||||
insert_activity(type, visibility, group, user, friends, non_friends, opts)
|
||||
insert_activity(type, visibility, group, users, opts)
|
||||
end,
|
||||
max_concurrency: @max_concurrency,
|
||||
timeout: 30_000
|
||||
|
@ -182,47 +183,46 @@ defp generate_activities(user, friends, non_friends, task_data, opts) do
|
|||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp insert_activity("simple", visibility, group, user, friends, non_friends, _opts) do
|
||||
{:ok, _activity} =
|
||||
defp insert_local_activity(visibility, group, users, status) do
|
||||
{:ok, _} =
|
||||
group
|
||||
|> get_actor(user, friends, non_friends)
|
||||
|> CommonAPI.post(%{status: "Simple status", visibility: visibility})
|
||||
|> get_actor(users)
|
||||
|> CommonAPI.post(%{status: status, visibility: visibility})
|
||||
end
|
||||
|
||||
defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do
|
||||
{:ok, _activity} =
|
||||
group
|
||||
|> get_actor(user, friends, non_friends)
|
||||
|> CommonAPI.post(%{
|
||||
status: "Simple status with emoji :firefox:",
|
||||
visibility: visibility
|
||||
})
|
||||
defp insert_remote_activity(visibility, group, users, status) do
|
||||
actor = get_actor(group, users)
|
||||
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
|
||||
{activity_data, object_data} = other_data(actor, status)
|
||||
|
||||
activity_data
|
||||
|> Map.merge(act_data)
|
||||
|> Map.put("object", Map.merge(object_data, obj_data))
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
|
||||
end
|
||||
|
||||
defp insert_activity("mentions", visibility, group, user, friends, non_friends, _opts) do
|
||||
defp user_mentions(users) do
|
||||
user_mentions =
|
||||
get_random_mentions(friends, Enum.random(0..3)) ++
|
||||
get_random_mentions(non_friends, Enum.random(0..3))
|
||||
Enum.reduce(
|
||||
@groups,
|
||||
[],
|
||||
fn group, acc ->
|
||||
acc ++ get_random_mentions(users[group], Enum.random(0..2))
|
||||
end
|
||||
)
|
||||
|
||||
user_mentions =
|
||||
if Enum.random([true, false]),
|
||||
do: ["@" <> user.nickname | user_mentions],
|
||||
do: ["@" <> users[:user].nickname | user_mentions],
|
||||
else: user_mentions
|
||||
|
||||
{:ok, _activity} =
|
||||
group
|
||||
|> get_actor(user, friends, non_friends)
|
||||
|> CommonAPI.post(%{
|
||||
status: Enum.join(user_mentions, ", ") <> " simple status with mentions",
|
||||
visibility: visibility
|
||||
})
|
||||
end
|
||||
|
||||
defp insert_activity("hell_thread", visibility, group, user, friends, non_friends, _opts) do
|
||||
mentions =
|
||||
defp hell_thread_mentions(users) do
|
||||
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
|
||||
cached =
|
||||
([user | Enum.take(friends, 10)] ++ Enum.take(non_friends, 10))
|
||||
@groups
|
||||
|> Enum.reduce([users[:user]], fn group, acc ->
|
||||
acc ++ Enum.take(users[group], 5)
|
||||
end)
|
||||
|> Enum.map(&"@#{&1.nickname}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
|
@ -231,18 +231,56 @@ defp insert_activity("hell_thread", visibility, group, user, friends, non_friend
|
|||
else
|
||||
{:ok, cached} -> cached
|
||||
end
|
||||
|
||||
{:ok, _activity} =
|
||||
group
|
||||
|> get_actor(user, friends, non_friends)
|
||||
|> CommonAPI.post(%{
|
||||
status: mentions <> " hell thread status",
|
||||
visibility: visibility
|
||||
})
|
||||
end
|
||||
|
||||
defp insert_activity("attachment", visibility, group, user, friends, non_friends, _opts) do
|
||||
actor = get_actor(group, user, friends, non_friends)
|
||||
defp insert_activity(:simple, visibility, group, users, _opts)
|
||||
when group in @remote_groups do
|
||||
insert_remote_activity(visibility, group, users, "Remote status")
|
||||
end
|
||||
|
||||
defp insert_activity(:simple, visibility, group, users, _opts) do
|
||||
insert_local_activity(visibility, group, users, "Simple status")
|
||||
end
|
||||
|
||||
defp insert_activity(:emoji, visibility, group, users, _opts)
|
||||
when group in @remote_groups do
|
||||
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
|
||||
end
|
||||
|
||||
defp insert_activity(:emoji, visibility, group, users, _opts) do
|
||||
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
|
||||
end
|
||||
|
||||
defp insert_activity(:mentions, visibility, group, users, _opts)
|
||||
when group in @remote_groups do
|
||||
mentions = user_mentions(users)
|
||||
|
||||
status = Enum.join(mentions, ", ") <> " remote status with mentions"
|
||||
|
||||
insert_remote_activity(visibility, group, users, status)
|
||||
end
|
||||
|
||||
defp insert_activity(:mentions, visibility, group, users, _opts) do
|
||||
mentions = user_mentions(users)
|
||||
|
||||
status = Enum.join(mentions, ", ") <> " simple status with mentions"
|
||||
insert_remote_activity(visibility, group, users, status)
|
||||
end
|
||||
|
||||
defp insert_activity(:hell_thread, visibility, group, users, _)
|
||||
when group in @remote_groups do
|
||||
mentions = hell_thread_mentions(users)
|
||||
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
|
||||
end
|
||||
|
||||
defp insert_activity(:hell_thread, visibility, group, users, _opts) do
|
||||
mentions = hell_thread_mentions(users)
|
||||
|
||||
insert_local_activity(visibility, group, users, mentions <> " hell thread status")
|
||||
end
|
||||
|
||||
defp insert_activity(:attachment, visibility, group, users, _opts) do
|
||||
actor = get_actor(group, users)
|
||||
|
||||
obj_data = %{
|
||||
"actor" => actor.ap_id,
|
||||
|
@ -268,67 +306,54 @@ defp insert_activity("attachment", visibility, group, user, friends, non_friends
|
|||
})
|
||||
end
|
||||
|
||||
defp insert_activity("tag", visibility, group, user, friends, non_friends, _opts) do
|
||||
{:ok, _activity} =
|
||||
group
|
||||
|> get_actor(user, friends, non_friends)
|
||||
|> CommonAPI.post(%{status: "Status with #tag", visibility: visibility})
|
||||
defp insert_activity(:tag, visibility, group, users, _opts) do
|
||||
insert_local_activity(visibility, group, users, "Status with #tag")
|
||||
end
|
||||
|
||||
defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do
|
||||
actor = get_actor(group, user, friends, non_friends)
|
||||
defp insert_activity(:like, visibility, group, users, opts) do
|
||||
actor = get_actor(group, users)
|
||||
|
||||
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
|
||||
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
|
||||
:ok
|
||||
else
|
||||
{:error, _} ->
|
||||
insert_activity("like", visibility, group, user, friends, non_friends, opts)
|
||||
insert_activity(:like, visibility, group, users, opts)
|
||||
|
||||
nil ->
|
||||
Process.sleep(15)
|
||||
insert_activity("like", visibility, group, user, friends, non_friends, opts)
|
||||
insert_activity(:like, visibility, group, users, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_activity("reblog", visibility, group, user, friends, non_friends, opts) do
|
||||
actor = get_actor(group, user, friends, non_friends)
|
||||
defp insert_activity(:reblog, visibility, group, users, opts) do
|
||||
actor = get_actor(group, users)
|
||||
|
||||
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
|
||||
{:ok, _activity, _object} <- CommonAPI.repeat(activity_id, actor) do
|
||||
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
|
||||
:ok
|
||||
else
|
||||
{:error, _} ->
|
||||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
|
||||
insert_activity(:reblog, visibility, group, users, opts)
|
||||
|
||||
nil ->
|
||||
Process.sleep(15)
|
||||
insert_activity("reblog", visibility, group, user, friends, non_friends, opts)
|
||||
insert_activity(:reblog, visibility, group, users, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_activity("simple_thread", visibility, group, user, friends, non_friends, _opts)
|
||||
when visibility in ["public", "unlisted", "private"] do
|
||||
actor = get_actor(group, user, friends, non_friends)
|
||||
tasks = get_reply_tasks(visibility, group)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility})
|
||||
|
||||
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
|
||||
insert_replies(tasks, visibility, user, friends, non_friends, acc)
|
||||
end
|
||||
|
||||
defp insert_activity("simple_thread", "direct", group, user, friends, non_friends, _opts) do
|
||||
actor = get_actor(group, user, friends, non_friends)
|
||||
defp insert_activity(:simple_thread, "direct", group, users, _opts) do
|
||||
actor = get_actor(group, users)
|
||||
tasks = get_reply_tasks("direct", group)
|
||||
|
||||
list =
|
||||
case group do
|
||||
"non_friends" ->
|
||||
Enum.take(non_friends, 3)
|
||||
:user ->
|
||||
group = Enum.random(@friends_groups)
|
||||
Enum.take(users[group], 3)
|
||||
|
||||
_ ->
|
||||
Enum.take(friends, 3)
|
||||
Enum.take(users[group], 3)
|
||||
end
|
||||
|
||||
data = Enum.map(list, &("@" <> &1.nickname))
|
||||
|
@ -339,40 +364,30 @@ defp insert_activity("simple_thread", "direct", group, user, friends, non_friend
|
|||
visibility: "direct"
|
||||
})
|
||||
|
||||
acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]}
|
||||
insert_direct_replies(tasks, user, list, acc)
|
||||
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
|
||||
insert_direct_replies(tasks, users[:user], list, acc)
|
||||
end
|
||||
|
||||
defp insert_activity("remote", _, "user", _, _, _, _), do: :ok
|
||||
defp insert_activity(:simple_thread, visibility, group, users, _opts) do
|
||||
actor = get_actor(group, users)
|
||||
tasks = get_reply_tasks(visibility, group)
|
||||
|
||||
defp insert_activity("remote", visibility, group, user, _friends, _non_friends, opts) do
|
||||
remote_friends =
|
||||
Users.get_users(user, limit: opts[:friends_used], local: :external, friends?: true)
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
|
||||
|
||||
remote_non_friends =
|
||||
Users.get_users(user, limit: opts[:non_friends_used], local: :external, friends?: false)
|
||||
|
||||
actor = get_actor(group, user, remote_friends, remote_non_friends)
|
||||
|
||||
{act_data, obj_data} = prepare_activity_data(actor, visibility, user)
|
||||
{activity_data, object_data} = other_data(actor)
|
||||
|
||||
activity_data
|
||||
|> Map.merge(act_data)
|
||||
|> Map.put("object", Map.merge(object_data, obj_data))
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
|
||||
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
|
||||
insert_replies(tasks, visibility, users, acc)
|
||||
end
|
||||
|
||||
defp get_actor("user", user, _friends, _non_friends), do: user
|
||||
defp get_actor("friends", _user, friends, _non_friends), do: Enum.random(friends)
|
||||
defp get_actor("non_friends", _user, _friends, non_friends), do: Enum.random(non_friends)
|
||||
defp get_actor(:user, %{user: user}), do: user
|
||||
defp get_actor(group, users), do: Enum.random(users[group])
|
||||
|
||||
defp other_data(actor) do
|
||||
defp other_data(actor, content) do
|
||||
%{host: host} = URI.parse(actor.ap_id)
|
||||
datetime = DateTime.utc_now()
|
||||
context_id = "http://#{host}:4000/contexts/#{UUID.generate()}"
|
||||
activity_id = "http://#{host}:4000/activities/#{UUID.generate()}"
|
||||
object_id = "http://#{host}:4000/objects/#{UUID.generate()}"
|
||||
context_id = "https://#{host}/contexts/#{UUID.generate()}"
|
||||
activity_id = "https://#{host}/activities/#{UUID.generate()}"
|
||||
object_id = "https://#{host}/objects/#{UUID.generate()}"
|
||||
|
||||
activity_data = %{
|
||||
"actor" => actor.ap_id,
|
||||
|
@ -389,7 +404,7 @@ defp other_data(actor) do
|
|||
"attributedTo" => actor.ap_id,
|
||||
"bcc" => [],
|
||||
"bto" => [],
|
||||
"content" => "Remote post",
|
||||
"content" => content,
|
||||
"context" => context_id,
|
||||
"conversation" => context_id,
|
||||
"emoji" => %{},
|
||||
|
@ -475,51 +490,65 @@ defp prepare_activity_data(_actor, "direct", mention) do
|
|||
{act_data, obj_data}
|
||||
end
|
||||
|
||||
defp get_reply_tasks("public", "user"), do: ~w(friend non_friend user)
|
||||
defp get_reply_tasks("public", "friends"), do: ~w(non_friend user friend)
|
||||
defp get_reply_tasks("public", "non_friends"), do: ~w(user friend non_friend)
|
||||
defp get_reply_tasks("public", :user) do
|
||||
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
|
||||
end
|
||||
|
||||
defp get_reply_tasks(visibility, "user") when visibility in ["unlisted", "private"],
|
||||
do: ~w(friend user friend)
|
||||
defp get_reply_tasks("public", group) when group in @friends_groups do
|
||||
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
|
||||
end
|
||||
|
||||
defp get_reply_tasks(visibility, "friends") when visibility in ["unlisted", "private"],
|
||||
do: ~w(user friend user)
|
||||
defp get_reply_tasks("public", group) when group in @non_friends_groups do
|
||||
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
|
||||
end
|
||||
|
||||
defp get_reply_tasks(visibility, "non_friends") when visibility in ["unlisted", "private"],
|
||||
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
|
||||
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
|
||||
end
|
||||
|
||||
defp get_reply_tasks(visibility, group)
|
||||
when visibility in ["unlisted", "private"] and group in @friends_groups do
|
||||
[:user, :friends_remote, :friends_local, :user]
|
||||
end
|
||||
|
||||
defp get_reply_tasks(visibility, group)
|
||||
when visibility in ["unlisted", "private"] and
|
||||
group in @non_friends_groups,
|
||||
do: []
|
||||
|
||||
defp get_reply_tasks("direct", "user"), do: ~w(friend user friend)
|
||||
defp get_reply_tasks("direct", "friends"), do: ~w(user friend user)
|
||||
defp get_reply_tasks("direct", "non_friends"), do: ~w(user non_friend user)
|
||||
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
|
||||
|
||||
defp insert_replies(tasks, visibility, user, friends, non_friends, acc) do
|
||||
defp get_reply_tasks("direct", group) when group in @friends_groups,
|
||||
do: [:user, group, :user]
|
||||
|
||||
defp get_reply_tasks("direct", group) when group in @non_friends_groups do
|
||||
[:user, :non_friends_remote, :user, :non_friends_local]
|
||||
end
|
||||
|
||||
defp insert_replies(tasks, visibility, users, acc) do
|
||||
Enum.reduce(tasks, acc, fn
|
||||
"friend", {id, data} ->
|
||||
friend = Enum.random(friends)
|
||||
insert_reply(friend, data, id, visibility)
|
||||
:user, {id, data} ->
|
||||
insert_reply(users[:user], data, id, visibility)
|
||||
|
||||
"non_friend", {id, data} ->
|
||||
non_friend = Enum.random(non_friends)
|
||||
insert_reply(non_friend, data, id, visibility)
|
||||
|
||||
"user", {id, data} ->
|
||||
insert_reply(user, data, id, visibility)
|
||||
group, {id, data} ->
|
||||
replier = Enum.random(users[group])
|
||||
insert_reply(replier, data, id, visibility)
|
||||
end)
|
||||
end
|
||||
|
||||
defp insert_direct_replies(tasks, user, list, acc) do
|
||||
Enum.reduce(tasks, acc, fn
|
||||
group, {id, data} when group in ["friend", "non_friend"] ->
|
||||
:user, {id, data} ->
|
||||
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
|
||||
{reply_id, data}
|
||||
|
||||
_, {id, data} ->
|
||||
actor = Enum.random(list)
|
||||
|
||||
{reply_id, _} =
|
||||
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
|
||||
|
||||
{reply_id, data}
|
||||
|
||||
"user", {id, data} ->
|
||||
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
|
||||
{reply_id, data}
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ defp fetch_timelines(user) do
|
|||
fetch_home_timeline(user)
|
||||
fetch_direct_timeline(user)
|
||||
fetch_public_timeline(user)
|
||||
fetch_public_timeline(user, :with_blocks)
|
||||
fetch_public_timeline(user, :local)
|
||||
fetch_public_timeline(user, :tag)
|
||||
fetch_notifications(user)
|
||||
|
@ -51,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
|
||||
|
||||
|
@ -69,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()
|
||||
|
||||
|
@ -89,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()
|
||||
)
|
||||
|
@ -109,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
|
||||
|
||||
|
@ -129,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
|
||||
|
@ -137,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
|
||||
|
@ -145,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
|
||||
|
@ -164,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()
|
||||
)
|
||||
|
@ -172,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
|
||||
|
||||
|
@ -222,24 +223,72 @@ 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
|
||||
|
||||
defp fetch_public_timeline(user, :with_blocks) do
|
||||
opts = opts_for_public_timeline(user)
|
||||
|
||||
remote_non_friends = Agent.get(:non_friends_remote, & &1)
|
||||
|
||||
Benchee.run(%{
|
||||
"public timeline without blocks" => fn ->
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
end
|
||||
})
|
||||
|
||||
Enum.each(remote_non_friends, fn non_friend ->
|
||||
{:ok, _} = User.block(user, non_friend)
|
||||
end)
|
||||
|
||||
user = User.get_by_id(user.id)
|
||||
|
||||
opts = Map.put(opts, :blocking_user, user)
|
||||
|
||||
Benchee.run(%{
|
||||
"public timeline with user block" => fn ->
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
end
|
||||
})
|
||||
|
||||
domains =
|
||||
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
|
||||
{:ok, _user} = User.unblock(user, non_friend)
|
||||
%{host: host} = URI.parse(non_friend.ap_id)
|
||||
[host | domains]
|
||||
end)
|
||||
|
||||
domains = Enum.uniq(domains)
|
||||
|
||||
Enum.each(domains, fn domain ->
|
||||
{:ok, _} = User.block_domain(user, domain)
|
||||
end)
|
||||
|
||||
user = User.get_by_id(user.id)
|
||||
opts = Map.put(opts, :blocking_user, user)
|
||||
|
||||
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(
|
||||
|
@ -250,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
|
||||
|
@ -269,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(
|
||||
|
@ -288,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()
|
||||
)
|
||||
|
@ -301,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(
|
||||
%{
|
||||
|
@ -317,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()
|
||||
)
|
||||
|
@ -328,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
|
||||
|
||||
|
@ -339,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(
|
||||
%{
|
||||
|
@ -461,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(
|
||||
|
@ -498,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
|
||||
},
|
||||
|
@ -524,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
|
||||
|
|
|
@ -27,7 +27,7 @@ def generate(opts \\ []) do
|
|||
|
||||
make_friends(main_user, opts[:friends])
|
||||
|
||||
Repo.get(User, main_user.id)
|
||||
User.get_by_id(main_user.id)
|
||||
end
|
||||
|
||||
def generate_users(max) do
|
||||
|
@ -166,4 +166,24 @@ defp run_stream(users, main_user) do
|
|||
)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
@spec prepare_users(User.t(), keyword()) :: map()
|
||||
def prepare_users(user, opts) do
|
||||
friends_limit = opts[:friends_used]
|
||||
non_friends_limit = opts[:non_friends_used]
|
||||
|
||||
%{
|
||||
user: user,
|
||||
friends_local: fetch_users(user, friends_limit, :local, true),
|
||||
friends_remote: fetch_users(user, friends_limit, :external, true),
|
||||
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
|
||||
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
|
||||
}
|
||||
end
|
||||
|
||||
defp fetch_users(user, limit, local, friends?) do
|
||||
user
|
||||
|> get_users(limit: limit, local: local, friends?: friends?)
|
||||
|> Enum.shuffle()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do
|
|||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.MastodonAPI.TimelineController
|
||||
|
||||
def run(_args) do
|
||||
Mix.Pleroma.start_pleroma()
|
||||
|
@ -37,7 +36,7 @@ def run(_args) do
|
|||
Benchee.run(
|
||||
%{
|
||||
"Hashtag fetching, any" => fn tags ->
|
||||
TimelineController.hashtag_fetching(
|
||||
hashtag_fetching(
|
||||
%{
|
||||
"any" => tags
|
||||
},
|
||||
|
@ -47,7 +46,7 @@ def run(_args) do
|
|||
end,
|
||||
# Will always return zero results because no overlapping hashtags are generated.
|
||||
"Hashtag fetching, all" => fn tags ->
|
||||
TimelineController.hashtag_fetching(
|
||||
hashtag_fetching(
|
||||
%{
|
||||
"all" => tags
|
||||
},
|
||||
|
@ -67,7 +66,7 @@ def run(_args) do
|
|||
Benchee.run(
|
||||
%{
|
||||
"Hashtag fetching" => fn tag ->
|
||||
TimelineController.hashtag_fetching(
|
||||
hashtag_fetching(
|
||||
%{
|
||||
"tag" => tag
|
||||
},
|
||||
|
@ -80,4 +79,35 @@ def run(_args) do
|
|||
time: 5
|
||||
)
|
||||
end
|
||||
|
||||
defp hashtag_fetching(params, user, local_only) do
|
||||
tags =
|
||||
[params["tag"], params["any"]]
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
|
||||
tag_all =
|
||||
params
|
||||
|> Map.get("all", [])
|
||||
|> Enum.map(&String.downcase(&1))
|
||||
|
||||
tag_reject =
|
||||
params
|
||||
|> 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)
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,7 +171,8 @@
|
|||
"application/ld+json" => ["activity+json"]
|
||||
}
|
||||
|
||||
config :tesla, adapter: Tesla.Adapter.Gun
|
||||
config :tesla, adapter: Tesla.Adapter.Hackney
|
||||
|
||||
# Configures http settings, upstream proxy etc.
|
||||
config :pleroma, :http,
|
||||
proxy_url: nil,
|
||||
|
@ -183,8 +184,9 @@
|
|||
name: "Pleroma",
|
||||
email: "example@example.com",
|
||||
notify_email: "noreply@example.com",
|
||||
description: "A Pleroma instance, an alternative fediverse server",
|
||||
description: "Pleroma: An efficient and flexible fediverse server",
|
||||
background_image: "/images/city.jpg",
|
||||
instance_thumbnail: "/instance/thumbnail.jpeg",
|
||||
limit: 5_000,
|
||||
chat_limit: 5_000,
|
||||
remote_limit: 100_000,
|
||||
|
@ -208,7 +210,6 @@
|
|||
Pleroma.Web.ActivityPub.Publisher
|
||||
],
|
||||
allow_relay: true,
|
||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
|
||||
public: true,
|
||||
quarantined_instances: [],
|
||||
managed_config: true,
|
||||
|
@ -219,8 +220,6 @@
|
|||
"text/markdown",
|
||||
"text/bbcode"
|
||||
],
|
||||
mrf_transparency: true,
|
||||
mrf_transparency_exclusions: [],
|
||||
autofollowed_nicknames: [],
|
||||
max_pinned_statuses: 1,
|
||||
attachment_links: false,
|
||||
|
@ -370,6 +369,8 @@
|
|||
|
||||
config :pleroma, :mrf_subchain, match_actor: %{}
|
||||
|
||||
config :pleroma, :mrf_activity_expiration, days: 365
|
||||
|
||||
config :pleroma, :mrf_vocabulary,
|
||||
accept: [],
|
||||
reject: []
|
||||
|
@ -384,7 +385,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]
|
||||
|
@ -405,6 +405,13 @@
|
|||
],
|
||||
whitelist: []
|
||||
|
||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||
method: :purge,
|
||||
headers: [],
|
||||
options: []
|
||||
|
||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script, script_path: nil
|
||||
|
||||
config :pleroma, :chat, enabled: true
|
||||
|
||||
config :phoenix, :format_encoders, json: Jason
|
||||
|
@ -683,6 +690,15 @@
|
|||
|
||||
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
|
||||
|
||||
config :pleroma, :mrf,
|
||||
policies: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
|
||||
transparency: true,
|
||||
transparency_exclusions: []
|
||||
|
||||
config :tzdata, :http_client, Pleroma.HTTP.Tzdata
|
||||
|
||||
config :ex_aws, http_client: Pleroma.HTTP.ExAws
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
|
@ -689,17 +689,6 @@
|
|||
type: :boolean,
|
||||
description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance"
|
||||
},
|
||||
%{
|
||||
key: :rewrite_policy,
|
||||
type: [:module, {:list, :module}],
|
||||
description:
|
||||
"A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
|
||||
suggestions:
|
||||
Generator.list_modules_in_dir(
|
||||
"lib/pleroma/web/activity_pub/mrf",
|
||||
"Elixir.Pleroma.Web.ActivityPub.MRF."
|
||||
)
|
||||
},
|
||||
%{
|
||||
key: :public,
|
||||
type: :boolean,
|
||||
|
@ -742,23 +731,6 @@
|
|||
"text/bbcode"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :mrf_transparency,
|
||||
label: "MRF transparency",
|
||||
type: :boolean,
|
||||
description:
|
||||
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
|
||||
},
|
||||
%{
|
||||
key: :mrf_transparency_exclusions,
|
||||
label: "MRF transparency exclusions",
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
|
||||
suggestions: [
|
||||
"exclusion.com"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :extended_nickname_format,
|
||||
type: :boolean,
|
||||
|
@ -979,7 +951,7 @@
|
|||
key: :instance_thumbnail,
|
||||
type: :string,
|
||||
description:
|
||||
"The instance thumbnail image. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html)",
|
||||
"The instance thumbnail can be any image that represents your instance and is used by some apps or services when they display information about your instance.",
|
||||
suggestions: ["/instance/thumbnail.jpeg"]
|
||||
}
|
||||
]
|
||||
|
@ -1471,6 +1443,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 +1595,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"]}
|
||||
# ]
|
||||
# ]
|
||||
# },
|
||||
|
@ -1637,6 +1622,31 @@
|
|||
"The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.",
|
||||
suggestions: ["https://example.com"]
|
||||
},
|
||||
%{
|
||||
key: :invalidation,
|
||||
type: :keyword,
|
||||
descpiption: "",
|
||||
suggestions: [
|
||||
enabled: true,
|
||||
provider: Pleroma.Web.MediaProxy.Invalidation.Script
|
||||
],
|
||||
children: [
|
||||
%{
|
||||
key: :enabled,
|
||||
type: :boolean,
|
||||
description: "Enables invalidate media cache"
|
||||
},
|
||||
%{
|
||||
key: :provider,
|
||||
type: :module,
|
||||
description: "Module which will be used to cache purge.",
|
||||
suggestions: [
|
||||
Pleroma.Web.MediaProxy.Invalidation.Script,
|
||||
Pleroma.Web.MediaProxy.Invalidation.Http
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :proxy_opts,
|
||||
type: :keyword,
|
||||
|
@ -1709,6 +1719,45 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||
type: :group,
|
||||
description: "HTTP invalidate settings",
|
||||
children: [
|
||||
%{
|
||||
key: :method,
|
||||
type: :atom,
|
||||
description: "HTTP method of request. Default: :purge"
|
||||
},
|
||||
%{
|
||||
key: :headers,
|
||||
type: {:list, :tuple},
|
||||
description: "HTTP headers of request.",
|
||||
suggestions: [{"x-refresh", 1}]
|
||||
},
|
||||
%{
|
||||
key: :options,
|
||||
type: :keyword,
|
||||
description: "Request options.",
|
||||
suggestions: [params: %{ts: "xxx"}]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Web.MediaProxy.Invalidation.Script,
|
||||
type: :group,
|
||||
description: "Script invalidate settings",
|
||||
children: [
|
||||
%{
|
||||
key: :script_path,
|
||||
type: :string,
|
||||
description: "Path to shell script. Which will run purge cache.",
|
||||
suggestions: ["./installation/nginx-cache-purge.sh.example"]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :gopher,
|
||||
|
@ -2091,9 +2140,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
|
||||
]
|
||||
},
|
||||
|
@ -3314,5 +3361,41 @@
|
|||
suggestions: [false]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf,
|
||||
type: :group,
|
||||
description: "General MRF settings",
|
||||
children: [
|
||||
%{
|
||||
key: :policies,
|
||||
type: [:module, {:list, :module}],
|
||||
description:
|
||||
"A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
|
||||
suggestions:
|
||||
Generator.list_modules_in_dir(
|
||||
"lib/pleroma/web/activity_pub/mrf",
|
||||
"Elixir.Pleroma.Web.ActivityPub.MRF."
|
||||
)
|
||||
},
|
||||
%{
|
||||
key: :transparency,
|
||||
label: "MRF transparency",
|
||||
type: :boolean,
|
||||
description:
|
||||
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
|
||||
},
|
||||
%{
|
||||
key: :transparency_exclusions,
|
||||
label: "MRF transparency exclusions",
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
|
||||
suggestions: [
|
||||
"exclusion.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -488,30 +488,52 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
|
||||
### Change the user's email, password, display and settings-related fields
|
||||
|
||||
- Params:
|
||||
- `email`
|
||||
- `password`
|
||||
- `name`
|
||||
- `bio`
|
||||
- `avatar`
|
||||
- `locked`
|
||||
- `no_rich_text`
|
||||
- `default_scope`
|
||||
- `banner`
|
||||
- `hide_follows`
|
||||
- `hide_followers`
|
||||
- `hide_followers_count`
|
||||
- `hide_follows_count`
|
||||
- `hide_favorites`
|
||||
- `allow_following_move`
|
||||
- `background`
|
||||
- `show_role`
|
||||
- `skip_thread_containment`
|
||||
- `fields`
|
||||
- `discoverable`
|
||||
- `actor_type`
|
||||
* Params:
|
||||
* `email`
|
||||
* `password`
|
||||
* `name`
|
||||
* `bio`
|
||||
* `avatar`
|
||||
* `locked`
|
||||
* `no_rich_text`
|
||||
* `default_scope`
|
||||
* `banner`
|
||||
* `hide_follows`
|
||||
* `hide_followers`
|
||||
* `hide_followers_count`
|
||||
* `hide_follows_count`
|
||||
* `hide_favorites`
|
||||
* `allow_following_move`
|
||||
* `background`
|
||||
* `show_role`
|
||||
* `skip_thread_containment`
|
||||
* `fields`
|
||||
* `discoverable`
|
||||
* `actor_type`
|
||||
|
||||
- Response: none (code `200`)
|
||||
* Responses:
|
||||
|
||||
Status: 200
|
||||
|
||||
```json
|
||||
{"status": "success"}
|
||||
```
|
||||
|
||||
Status: 400
|
||||
|
||||
```json
|
||||
{"errors":
|
||||
{"actor_type": "is invalid"},
|
||||
{"email": "has invalid format"},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Status: 404
|
||||
|
||||
```json
|
||||
{"error": "Not found"}
|
||||
```
|
||||
|
||||
## `GET /api/pleroma/admin/reports`
|
||||
|
||||
|
@ -531,7 +553,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
|
||||
```json
|
||||
{
|
||||
"totalReports" : 1,
|
||||
"total" : 1,
|
||||
"reports": [
|
||||
{
|
||||
"account": {
|
||||
|
@ -752,7 +774,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
||||
- On success: `204`, empty response
|
||||
|
||||
## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
|
||||
## `DELETE /api/pleroma/admin/reports/:report_id/notes/:id`
|
||||
|
||||
### Delete report note
|
||||
|
||||
|
@ -1096,6 +1118,10 @@ Loads json generated from `config/descriptions.exs`.
|
|||
|
||||
### Stats
|
||||
|
||||
- Query Params:
|
||||
- *optional* `instance`: **string** instance hostname (without protocol) to get stats for
|
||||
- Example: `https://mypleroma.org/api/pleroma/admin/stats?instance=lain.com`
|
||||
|
||||
- Response:
|
||||
|
||||
```json
|
||||
|
@ -1209,3 +1235,65 @@ Loads json generated from `config/descriptions.exs`.
|
|||
- On success: `204`, empty response
|
||||
- On failure:
|
||||
- 400 Bad Request `"Invalid parameters"` when `status` is missing
|
||||
|
||||
## `GET /api/pleroma/admin/media_proxy_caches`
|
||||
|
||||
### Get a list of all banned MediaProxy URLs in Cachex
|
||||
|
||||
- Authentication: required
|
||||
- Params:
|
||||
- *optional* `page`: **integer** page number
|
||||
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
|
||||
|
||||
- Response:
|
||||
|
||||
``` json
|
||||
{
|
||||
"urls": [
|
||||
"http://example.com/media/a688346.jpg",
|
||||
"http://example.com/media/fb1f4d.jpg"
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## `POST /api/pleroma/admin/media_proxy_caches/delete`
|
||||
|
||||
### Remove a banned MediaProxy URL from Cachex
|
||||
|
||||
- Authentication: required
|
||||
- Params:
|
||||
- `urls` (array)
|
||||
|
||||
- Response:
|
||||
|
||||
``` json
|
||||
{
|
||||
"urls": [
|
||||
"http://example.com/media/a688346.jpg",
|
||||
"http://example.com/media/fb1f4d.jpg"
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## `POST /api/pleroma/admin/media_proxy_caches/purge`
|
||||
|
||||
### Purge a MediaProxy URL
|
||||
|
||||
- Authentication: required
|
||||
- Params:
|
||||
- `urls` (array)
|
||||
- `ban` (boolean)
|
||||
|
||||
- Response:
|
||||
|
||||
``` json
|
||||
{
|
||||
"urls": [
|
||||
"http://example.com/media/a688346.jpg",
|
||||
"http://example.com/media/fb1f4d.jpg"
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
|
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.
|
|
@ -6,10 +6,6 @@ A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma
|
|||
|
||||
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are lexically sortable strings
|
||||
|
||||
## Attachment cap
|
||||
|
||||
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
|
||||
|
||||
## Timelines
|
||||
|
||||
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
||||
|
@ -32,12 +28,20 @@ Has these additional fields under the `pleroma` object:
|
|||
- `thread_muted`: true if the thread the post belongs to is muted
|
||||
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
|
||||
|
||||
## Attachments
|
||||
## Media Attachments
|
||||
|
||||
Has these additional fields under the `pleroma` object:
|
||||
|
||||
- `mime_type`: mime type of the attachment.
|
||||
|
||||
### Attachment cap
|
||||
|
||||
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
|
||||
|
||||
### Limitations
|
||||
|
||||
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
|
||||
|
||||
## Accounts
|
||||
|
||||
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
|
||||
|
@ -226,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.
|
||||
|
|
|
@ -449,18 +449,44 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
|||
* Response: JSON, list with updated files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
|
||||
|
||||
## `GET /api/pleroma/emoji/packs`
|
||||
|
||||
### Lists local custom emoji packs
|
||||
|
||||
* Method `GET`
|
||||
* Authentication: not required
|
||||
* Params: None
|
||||
* Response: JSON, "ok" and 200 status and the JSON hashmap of pack name to pack contents
|
||||
* Params:
|
||||
* `page`: page number for packs (default 1)
|
||||
* `page_size`: page size for packs (default 50)
|
||||
* Response: `packs` key with JSON hashmap of pack name to pack contents and `count` key for count of packs.
|
||||
|
||||
```json
|
||||
{
|
||||
"packs": {
|
||||
"pack_name": {...}, // pack contents
|
||||
...
|
||||
},
|
||||
"count": 0 // packs count
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/pleroma/emoji/packs/:name`
|
||||
|
||||
### Get pack.json for the pack
|
||||
|
||||
* Method `GET`
|
||||
* Authentication: not required
|
||||
* Params: None
|
||||
* Response: JSON, pack json with `files` and `pack` keys with 200 status or 404 if the pack does not exist
|
||||
* Params:
|
||||
* `page`: page number for files (default 1)
|
||||
* `page_size`: page size for files (default 30)
|
||||
* Response: JSON, pack json with `files`, `files_count` and `pack` keys with 200 status or 404 if the pack does not exist.
|
||||
|
||||
```json
|
||||
{
|
||||
"files": {...},
|
||||
"files_count": 0, // emoji count in pack
|
||||
"pack": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/pleroma/emoji/packs/:name/archive`
|
||||
### Requests a local pack archive from the instance
|
||||
|
|
|
@ -69,3 +69,32 @@ mix pleroma.database update_users_following_followers_counts
|
|||
```sh tab="From Source"
|
||||
mix pleroma.database fix_likes_collections
|
||||
```
|
||||
|
||||
## Vacuum the database
|
||||
|
||||
### Analyze
|
||||
|
||||
Running an `analyze` vacuum job can improve performance by updating statistics used by the query planner. **It is safe to cancel this.**
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database vacuum analyze
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database vacuum analyze
|
||||
```
|
||||
|
||||
### Full
|
||||
|
||||
Running a `full` vacuum job rebuilds your entire database by reading all of the data and rewriting it into smaller
|
||||
and more compact files with an optimized layout. This process will take a long time and use additional disk space as
|
||||
it builds the files side-by-side the existing database files. It can make your database faster and use less disk space,
|
||||
but should only be run if necessary. **It is safe to cancel this.**
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database vacuum full
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database vacuum full
|
||||
```
|
|
@ -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`.
|
|
@ -42,6 +42,12 @@ Feel free to contact us to be added to this list!
|
|||
- Platforms: SailfishOS
|
||||
- Features: No Streaming
|
||||
|
||||
### Husky
|
||||
- Source code: <https://git.mentality.rip/FWGS/Husky>
|
||||
- Contact: [@Husky@enigmatic.observer](https://enigmatic.observer/users/Husky)
|
||||
- Platforms: Android
|
||||
- Features: No Streaming, Emoji Reactions, Text Formatting, FE Stickers
|
||||
|
||||
### Nekonium
|
||||
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
|
||||
- Source: <https://gogs.gdgd.jp.net/lin/nekonium>
|
||||
|
|
|
@ -36,30 +36,15 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
|
||||
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
|
||||
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
|
||||
* `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.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)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
|
||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
|
||||
* `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.
|
||||
* `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).
|
||||
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||
* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
|
||||
older software for theses nicknames.
|
||||
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
|
||||
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
|
||||
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses.
|
||||
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
|
||||
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
|
||||
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
|
||||
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
|
||||
|
@ -77,11 +62,30 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
|
||||
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
|
||||
|
||||
## Message rewrite facility
|
||||
|
||||
### :mrf
|
||||
* `policies`: 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.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)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
|
||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
|
||||
* `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)).
|
||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||
|
||||
## Federation
|
||||
### MRF policies
|
||||
|
||||
!!! note
|
||||
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `rewrite_policy` under [:instance](#instance) section.
|
||||
Configuring MRF policies is not enough for them to take effect. You have to enable them by specifying their module in `policies` under [:mrf](#mrf) section.
|
||||
|
||||
#### :mrf_simple
|
||||
* `media_removal`: List of instances to remove media from.
|
||||
|
@ -137,8 +141,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 +159,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
|
||||
|
@ -262,7 +271,7 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
|||
|
||||
#### Pleroma.Web.MediaProxy.Invalidation.Script
|
||||
|
||||
This strategy allow perform external bash script to purge cache.
|
||||
This strategy allow perform external shell script to purge cache.
|
||||
Urls of attachments pass to script as arguments.
|
||||
|
||||
* `script_path`: path to external script.
|
||||
|
@ -278,8 +287,8 @@ config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
|
|||
This strategy allow perform custom http request to purge cache.
|
||||
|
||||
* `method`: http method. default is `purge`
|
||||
* `headers`: http headers. default is empty
|
||||
* `options`: request options. default is empty
|
||||
* `headers`: http headers.
|
||||
* `options`: request options.
|
||||
|
||||
Example:
|
||||
```elixir
|
||||
|
@ -963,13 +972,13 @@ config :pleroma, :database_config_whitelist, [
|
|||
|
||||
Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||
|
||||
* `timelines` - public and federated timelines
|
||||
* `local` - public timeline
|
||||
* `timelines`: public and federated timelines
|
||||
* `local`: public timeline
|
||||
* `federated`
|
||||
* `profiles` - user profiles
|
||||
* `profiles`: user profiles
|
||||
* `local`
|
||||
* `remote`
|
||||
* `activities` - statuses
|
||||
* `activities`: statuses
|
||||
* `local`
|
||||
* `remote`
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ Example of `my-awesome-theme.json` where we add the name "My Awesome Theme"
|
|||
|
||||
### Set as default theme
|
||||
|
||||
Now we can set the new theme as default in the [Pleroma FE configuration](General-tips-for-customizing-Pleroma-FE.md).
|
||||
Now we can set the new theme as default in the [Pleroma FE configuration](../../../frontend/CONFIGURATION).
|
||||
|
||||
Example of adding the new theme in the back-end config files
|
||||
```elixir
|
||||
|
|
|
@ -34,9 +34,9 @@ config :pleroma, :instance,
|
|||
To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :instance,
|
||||
config :pleroma, :mrf,
|
||||
[...]
|
||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy
|
||||
policies: Pleroma.Web.ActivityPub.MRF.SimplePolicy
|
||||
```
|
||||
|
||||
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
|
||||
|
@ -58,8 +58,8 @@ Servers should be configured as lists.
|
|||
This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :instance,
|
||||
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
|
||||
config :pleroma, :mrf,
|
||||
policies: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
|
||||
|
||||
config :pleroma, :mrf_simple,
|
||||
media_removal: ["illegalporn.biz"],
|
||||
|
@ -75,7 +75,7 @@ The effects of MRF policies can be very drastic. It is important to use this fun
|
|||
|
||||
## Writing your own MRF Policy
|
||||
|
||||
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting.
|
||||
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting.
|
||||
|
||||
For example, here is a sample policy module which rewrites all messages to "new message content":
|
||||
|
||||
|
@ -125,8 +125,8 @@ end
|
|||
If you save this file as `lib/pleroma/web/activity_pub/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :instance,
|
||||
rewrite_policy: [
|
||||
config :pleroma, :mrf,
|
||||
policies: [
|
||||
Pleroma.Web.ActivityPub.MRF.SimplePolicy,
|
||||
Pleroma.Web.ActivityPub.MRF.RewritePolicy
|
||||
]
|
||||
|
|
|
@ -33,6 +33,6 @@ as soon as the post is received by your instance.
|
|||
Add to your `prod.secret.exs`:
|
||||
|
||||
```
|
||||
config :pleroma, :instance,
|
||||
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
|
||||
config :pleroma, :mrf,
|
||||
policies: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
|
||||
```
|
||||
|
|
|
@ -20,4 +20,4 @@ This document contains notes and guidelines for Pleroma developers.
|
|||
|
||||
## Auth-related configuration, OAuth consumer mode etc.
|
||||
|
||||
See `Authentication` section of [`docs/configuration/cheatsheet.md`](docs/configuration/cheatsheet.md#authentication).
|
||||
See `Authentication` section of [the configuration cheatsheet](configuration/cheatsheet.md#authentication).
|
||||
|
|
26
docs/index.md
Normal file
26
docs/index.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Introduction to Pleroma
|
||||
## What is Pleroma?
|
||||
Pleroma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3.
|
||||
It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
|
||||
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
|
||||
One account on an instance is enough to talk to the entire fediverse!
|
||||
|
||||
## How can I use it?
|
||||
|
||||
Pleroma instances are already widely deployed, a list can be found at <https://the-federation.info/pleroma> and <https://fediverse.network/pleroma>.
|
||||
|
||||
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
|
||||
Installation instructions can be found in the installation section of these docs.
|
||||
|
||||
## I got an account, now what?
|
||||
Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register)
|
||||
|
||||
### Pleroma-FE
|
||||
The default front-end used by Pleroma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](../frontend).
|
||||
|
||||
### Mastodon interface
|
||||
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
|
||||
Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
|
||||
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
|
||||
|
||||
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.
|
|
@ -225,10 +225,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
|
|||
|
||||
#### Further reading
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
|
|
|
@ -200,10 +200,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
|
|||
|
||||
#### Further reading
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
|
|
|
@ -38,8 +38,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
|
|||
* Download and add the Erlang repository:
|
||||
|
||||
```shell
|
||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
|
||||
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
|
||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
|
||||
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
|
||||
```
|
||||
|
||||
* Install Elixir and Erlang:
|
||||
|
@ -186,10 +186,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
|
|||
|
||||
#### Further reading
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@ sudo apt install git build-essential postgresql postgresql-contrib
|
|||
|
||||
* Erlangのリポジトリをダウンロードおよびインストールします。
|
||||
```
|
||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
|
||||
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
|
||||
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb
|
||||
sudo dpkg -i /tmp/erlang-solutions_2.0_all.deb
|
||||
```
|
||||
|
||||
* ElixirとErlangをインストールします、
|
||||
|
@ -175,10 +175,7 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
|
|||
|
||||
#### その他の設定とカスタマイズ
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## 質問ある?
|
||||
|
||||
|
|
5
docs/installation/further_reading.include
Normal file
5
docs/installation/further_reading.include
Normal file
|
@ -0,0 +1,5 @@
|
|||
* [How Federation Works/Why is my Federated Timeline empty?](https://blog.soykaf.com/post/how-federation-works/)
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
|
@ -283,10 +283,7 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
|
|||
|
||||
#### Further reading
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
|
|
|
@ -196,3 +196,11 @@ incorrect timestamps. You should have ntpd running.
|
|||
## Instances running NetBSD
|
||||
|
||||
* <https://catgirl.science>
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**.
|
||||
|
|
|
@ -242,3 +242,11 @@ If your instance is up and running, you can create your first user with administ
|
|||
```
|
||||
LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**.
|
||||
|
|
|
@ -270,10 +270,7 @@ This will create an account withe the username of 'joeuser' with the email addre
|
|||
|
||||
## Further reading
|
||||
|
||||
* [Backup your instance](../administration/backup.md)
|
||||
* [Hardening your instance](../configuration/hardening.md)
|
||||
* [How to activate mediaproxy](../configuration/howto_mediaproxy.md)
|
||||
* [Updating your instance](../administration/updating.md)
|
||||
{! backend/installation/further_reading.include !}
|
||||
|
||||
## Questions
|
||||
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
# Introduction to Pleroma
|
||||
## What is Pleroma?
|
||||
Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
|
||||
It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
|
||||
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
|
||||
One account on an instance is enough to talk to the entire fediverse!
|
||||
|
||||
## How can I use it?
|
||||
|
||||
Pleroma instances are already widely deployed, a list can be found at <http://distsn.org/pleroma-instances.html>. Information on all existing fediverse instances can be found at <https://fediverse.network/>.
|
||||
|
||||
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
|
||||
Installation instructions can be found in the installation section of these docs.
|
||||
|
||||
## I got an account, now what?
|
||||
Great! Now you can explore the fediverse! Open the login page for your Pleroma instance (e.g. <https://pleroma.soykaf.com>) and login with your username and password. (If you don't have an account yet, click on Register)
|
||||
|
||||
At this point you will have two columns in front of you.
|
||||
|
||||
### Left column
|
||||
|
||||
- first block: here you can see your avatar, your nickname and statistics (Statuses, Following, Followers). Clicking your profile pic will open your profile.
|
||||
Under that you have a text form which allows you to post new statuses. The number on the bottom of the text form is a character counter, every instance can have a different character limit (the default is 5000).
|
||||
If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person.
|
||||
Under the text form there are also several visibility options and there is the option to use rich text.
|
||||
Under that the icon on the left is for uploading media files and attach them to your post. There is also an emoji-picker and an option to post a poll.
|
||||
To post your status, simply press Submit.
|
||||
On the top right you will also see a wrench icon. This opens your personal settings.
|
||||
|
||||
- second block: Here you can switch between the different timelines:
|
||||
- Timeline: all the people that you follow
|
||||
- Interactions: here you can switch between different timelines where there was interaction with your account. There is Mentions, Repeats and Favorites, and New follows
|
||||
- Direct Messages: these are the Direct Messages sent to you
|
||||
- Public Timeline: all the statutes from the local instance
|
||||
- The Whole Known Network: all public posts the instance knows about, both local and remote!
|
||||
- About: This isn't a Timeline but shows relevant info about the instance. You can find a list of the moderators and admins, Terms of Service, MRF policies and enabled features.
|
||||
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable and by default has links to the pleroma-fe and Mastodon-fe.
|
||||
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses.
|
||||
|
||||
### Right column
|
||||
This is where the interesting stuff happens!
|
||||
Depending on the timeline you will see different statuses, but each status has a standard structure:
|
||||
|
||||
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile.
|
||||
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
|
||||
- An arrow icon allows you to open the status on the instance where it's originating from.
|
||||
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.
|
||||
- Three buttons (left to right): Reply, Repeat, Favorite. There is also a forth button, this is a dropdown menu for simple moderation like muting the conversation or, if you have moderation rights, delete the status from the server.
|
||||
|
||||
### Top right
|
||||
|
||||
- The magnifier icon opens the search screen where you can search for statuses, people and hashtags. It's also possible to import statusses from remote servers by pasting the url to the post in the search field.
|
||||
- The gear icon gives you general settings
|
||||
- If you have admin rights, you'll see an icon that opens the admin interface
|
||||
- The last icon is to log out
|
||||
|
||||
### Bottom right
|
||||
On the bottom right you have a chatbox. Here you can communicate with people on the same instance in realtime. It is local-only, for now, but there are plans to make it extendable to the entire fediverse!
|
||||
|
||||
### Mastodon interface
|
||||
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
|
||||
Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
|
||||
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.
|
||||
|
||||
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,7 @@ CACHE_DIRECTORY="/tmp/pleroma-media-cache"
|
|||
## $3 - (optional) the number of parallel processes to run for grep.
|
||||
get_cache_files() {
|
||||
local max_parallel=${3-16}
|
||||
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E Rl "^KEY:.*$1" | sort -u
|
||||
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E -Rl "^KEY:.*$1" | sort -u
|
||||
}
|
||||
|
||||
## Removes an item from the given cache zone.
|
||||
|
@ -37,4 +37,4 @@ purge() {
|
|||
|
||||
}
|
||||
|
||||
purge $1
|
||||
purge $@
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -52,6 +52,7 @@ def migrate_to_db(file_path \\ nil) do
|
|||
|
||||
defp do_migrate_to_db(config_file) do
|
||||
if File.exists?(config_file) do
|
||||
shell_info("Migrating settings from file: #{Path.expand(config_file)}")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
|
||||
|
||||
|
@ -72,8 +73,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 +131,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
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Mix.Tasks.Pleroma.Database do
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Maintenance
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -34,13 +35,7 @@ def run(["remove_embedded_objects" | args]) do
|
|||
)
|
||||
|
||||
if Keyword.get(options, :vacuum) do
|
||||
Logger.info("Runnning VACUUM FULL")
|
||||
|
||||
Repo.query!(
|
||||
"vacuum full;",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
Maintenance.vacuum("full")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,13 +89,7 @@ def run(["prune_objects" | args]) do
|
|||
|> Repo.delete_all(timeout: :infinity)
|
||||
|
||||
if Keyword.get(options, :vacuum) do
|
||||
Logger.info("Runnning VACUUM FULL")
|
||||
|
||||
Repo.query!(
|
||||
"vacuum full;",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
Maintenance.vacuum("full")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -135,4 +124,10 @@ def run(["fix_likes_collections"]) do
|
|||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
def run(["vacuum", args]) do
|
||||
start_pleroma()
|
||||
|
||||
Maintenance.vacuum(args)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ def run(["ls-packs" | args]) do
|
|||
{options, [], []} = parse_global_opts(args)
|
||||
|
||||
url_or_path = options[:manifest] || default_manifest()
|
||||
manifest = fetch_manifest(url_or_path)
|
||||
manifest = fetch_and_decode(url_or_path)
|
||||
|
||||
Enum.each(manifest, fn {name, info} ->
|
||||
to_print = [
|
||||
|
@ -42,12 +42,12 @@ def run(["get-packs" | args]) do
|
|||
|
||||
url_or_path = options[:manifest] || default_manifest()
|
||||
|
||||
manifest = fetch_manifest(url_or_path)
|
||||
manifest = fetch_and_decode(url_or_path)
|
||||
|
||||
for pack_name <- pack_names do
|
||||
if Map.has_key?(manifest, pack_name) do
|
||||
pack = manifest[pack_name]
|
||||
src_url = pack["src"]
|
||||
src = pack["src"]
|
||||
|
||||
IO.puts(
|
||||
IO.ANSI.format([
|
||||
|
@ -57,11 +57,11 @@ def run(["get-packs" | args]) do
|
|||
:normal,
|
||||
" from ",
|
||||
:underline,
|
||||
src_url
|
||||
src
|
||||
])
|
||||
)
|
||||
|
||||
binary_archive = Tesla.get!(client(), src_url).body
|
||||
{:ok, binary_archive} = fetch(src)
|
||||
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
|
||||
|
||||
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
|
||||
|
@ -74,8 +74,8 @@ def run(["get-packs" | args]) do
|
|||
raise "Bad SHA256 for #{pack_name}"
|
||||
end
|
||||
|
||||
# The url specified in files should be in the same directory
|
||||
files_url =
|
||||
# The location specified in files should be in the same directory
|
||||
files_loc =
|
||||
url_or_path
|
||||
|> Path.dirname()
|
||||
|> Path.join(pack["files"])
|
||||
|
@ -88,11 +88,11 @@ def run(["get-packs" | args]) do
|
|||
:normal,
|
||||
" from ",
|
||||
:underline,
|
||||
files_url
|
||||
files_loc
|
||||
])
|
||||
)
|
||||
|
||||
files = Tesla.get!(client(), files_url).body |> Jason.decode!()
|
||||
files = fetch_and_decode(files_loc)
|
||||
|
||||
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
|
||||
|
||||
|
@ -237,15 +237,25 @@ def run(["gen-pack" | args]) do
|
|||
end
|
||||
end
|
||||
|
||||
defp fetch_manifest(from) do
|
||||
Jason.decode!(
|
||||
if String.starts_with?(from, "http") do
|
||||
Tesla.get!(client(), from).body
|
||||
else
|
||||
File.read!(from)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch("http" <> _ = from) do
|
||||
with {:ok, %{body: body}} <- Tesla.get(client(), from) do
|
||||
{:ok, body}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch(path), do: File.read(path)
|
||||
|
||||
defp parse_global_opts(args) do
|
||||
OptionParser.parse(
|
||||
|
|
|
@ -17,30 +17,53 @@ defmodule Mix.Tasks.Pleroma.RefreshCounterCache do
|
|||
def run([]) do
|
||||
Mix.Pleroma.start_pleroma()
|
||||
|
||||
["public", "unlisted", "private", "direct"]
|
||||
|> Enum.each(fn visibility ->
|
||||
count = status_visibility_count_query(visibility)
|
||||
name = "status_visibility_#{visibility}"
|
||||
CounterCache.set(name, count)
|
||||
Mix.Pleroma.shell_info("Set #{name} to #{count}")
|
||||
instances =
|
||||
Activity
|
||||
|> distinct([a], true)
|
||||
|> select([a], fragment("split_part(?, '/', 3)", a.actor))
|
||||
|> Repo.all()
|
||||
|
||||
instances
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.each(fn {instance, i} ->
|
||||
counters = instance_counters(instance)
|
||||
CounterCache.set(instance, counters)
|
||||
|
||||
Mix.Pleroma.shell_info(
|
||||
"[#{i}/#{length(instances)}] Setting #{instance} counters: #{inspect(counters)}"
|
||||
)
|
||||
end)
|
||||
|
||||
Mix.Pleroma.shell_info("Done")
|
||||
end
|
||||
|
||||
defp status_visibility_count_query(visibility) do
|
||||
defp instance_counters(instance) do
|
||||
counters = %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
|
||||
|
||||
Activity
|
||||
|> where(
|
||||
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|
||||
|> where([a], fragment("split_part(?, '/', 3) = ?", a.actor, ^instance))
|
||||
|> select(
|
||||
[a],
|
||||
fragment(
|
||||
"activity_visibility(?, ?, ?) = ?",
|
||||
{fragment(
|
||||
"activity_visibility(?, ?, ?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
a.data
|
||||
), count(a.id)}
|
||||
)
|
||||
|> group_by(
|
||||
[a],
|
||||
fragment(
|
||||
"activity_visibility(?, ?, ?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|
||||
|> Repo.aggregate(:count, :id, timeout: :timer.minutes(30))
|
||||
|> Repo.all(timeout: :timer.minutes(30))
|
||||
|> Enum.reduce(counters, fn {visibility, count}, acc ->
|
||||
Map.put(acc, visibility, count)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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: []
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ def start(_type, _args) do
|
|||
Pleroma.HTML.compile_scrubbers()
|
||||
Config.DeprecationWarnings.warn()
|
||||
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
|
||||
Pleroma.Repo.check_migrations_applied!()
|
||||
Pleroma.ApplicationRequirements.verify!()
|
||||
setup_instrumenters()
|
||||
load_custom_modules()
|
||||
|
||||
|
@ -148,7 +148,8 @@ defp cachex_children do
|
|||
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
|
||||
build_cachex("web_resp", limit: 2500),
|
||||
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
|
||||
build_cachex("failed_proxy_url", limit: 2500)
|
||||
build_cachex("failed_proxy_url", limit: 2500),
|
||||
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
107
lib/pleroma/application_requirements.ex
Normal file
107
lib/pleroma/application_requirements.ex
Normal file
|
@ -0,0 +1,107 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationRequirements do
|
||||
@moduledoc """
|
||||
The module represents the collection of validations to runs before start server.
|
||||
"""
|
||||
|
||||
defmodule VerifyError, do: defexception([:message])
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
||||
@spec verify!() :: :ok | VerifyError.t()
|
||||
def verify! do
|
||||
:ok
|
||||
|> check_migrations_applied!()
|
||||
|> check_rum!()
|
||||
|> handle_result()
|
||||
end
|
||||
|
||||
defp handle_result(:ok), do: :ok
|
||||
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
|
||||
|
||||
# Checks for pending migrations.
|
||||
#
|
||||
def check_migrations_applied!(:ok) do
|
||||
unless Pleroma.Config.get(
|
||||
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
|
||||
false
|
||||
) do
|
||||
{_, res, _} =
|
||||
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
|
||||
down_migrations =
|
||||
Ecto.Migrator.migrations(repo)
|
||||
|> Enum.reject(fn
|
||||
{:up, _, _} -> true
|
||||
{:down, _, _} -> false
|
||||
end)
|
||||
|
||||
if length(down_migrations) > 0 do
|
||||
down_migrations_text =
|
||||
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
|
||||
|
||||
Logger.error(
|
||||
"The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
|
||||
)
|
||||
|
||||
{:error, "Unapplied Migrations detected"}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
res
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_migrations_applied!(result), do: result
|
||||
|
||||
# Checks for settings of RUM indexes.
|
||||
#
|
||||
defp check_rum!(:ok) do
|
||||
{_, res, _} =
|
||||
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
|
||||
migrate =
|
||||
from(o in "columns",
|
||||
where: o.table_name == "objects",
|
||||
where: o.column_name == "fts_content"
|
||||
)
|
||||
|> repo.exists?(prefix: "information_schema")
|
||||
|
||||
setting = Pleroma.Config.get([:database, :rum_enabled], false)
|
||||
|
||||
do_check_rum!(setting, migrate)
|
||||
end)
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
defp check_rum!(result), do: result
|
||||
|
||||
defp do_check_rum!(setting, migrate) do
|
||||
case {setting, migrate} do
|
||||
{true, false} ->
|
||||
Logger.error(
|
||||
"Use `RUM` index is enabled, but were not applied migrations for it.\nIf you want to start Pleroma anyway, set\nconfig :pleroma, :database, rum_enabled: false\nOtherwise apply the following migrations:\n`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`"
|
||||
)
|
||||
|
||||
{:error, "Unapplied RUM Migrations detected"}
|
||||
|
||||
{false, true} ->
|
||||
Logger.error(
|
||||
"Detected applied migrations to use `RUM` index, but `RUM` isn't enable in settings.\nIf you want to use `RUM`, set\nconfig :pleroma, :database, rum_enabled: true\nOtherwise roll `RUM` migrations back.\n`mix ecto.rollback --migrations-path priv/repo/optional_migrations/rum_indexing/`"
|
||||
)
|
||||
|
||||
{:error, "RUM Migrations detected"}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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,57 +130,55 @@ 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
|
||||
|
||||
@spec delete(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
|
||||
def delete(%ConfigDB{} = config), do: Repo.delete(config)
|
||||
|
||||
def delete(params) do
|
||||
search_opts = Map.delete(params, :subkeys)
|
||||
|
||||
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 +194,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 +228,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 +294,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 +375,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
|
||||
|
|
|
@ -3,10 +3,25 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarnings do
|
||||
alias Pleroma.Config
|
||||
|
||||
require Logger
|
||||
alias Pleroma.Config
|
||||
|
||||
@type config_namespace() :: [atom()]
|
||||
@type config_map() :: {config_namespace(), config_namespace(), String.t()}
|
||||
|
||||
@mrf_config_map [
|
||||
{[:instance, :rewrite_policy], [:mrf, :policies],
|
||||
"\n* `config :pleroma, :instance, rewrite_policy` is now `config :pleroma, :mrf, policies`"},
|
||||
{[:instance, :mrf_transparency], [:mrf, :transparency],
|
||||
"\n* `config :pleroma, :instance, mrf_transparency` is now `config :pleroma, :mrf, transparency`"},
|
||||
{[:instance, :mrf_transparency_exclusions], [:mrf, :transparency_exclusions],
|
||||
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
|
||||
]
|
||||
|
||||
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 +29,59 @@ 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()
|
||||
check_old_mrf_config()
|
||||
end
|
||||
|
||||
def check_old_mrf_config do
|
||||
warning_preface = """
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using old namespaces for MRF configuration. They should work for now, but you are advised to change to new namespaces to prevent possible issues later:
|
||||
"""
|
||||
|
||||
move_namespace_and_warn(@mrf_config_map, warning_preface)
|
||||
end
|
||||
|
||||
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok
|
||||
def move_namespace_and_warn(config_map, warning_preface) do
|
||||
warning =
|
||||
Enum.reduce(config_map, "", fn
|
||||
{old, new, err_msg}, acc ->
|
||||
old_config = Config.get(old)
|
||||
|
||||
if old_config do
|
||||
Config.put(new, old_config)
|
||||
acc <> err_msg
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
if warning != "" do
|
||||
Logger.warn(warning_preface <> warning)
|
||||
end
|
||||
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 =
|
||||
|
|
|
@ -24,6 +24,6 @@ defmodule Pleroma.Constants do
|
|||
|
||||
const(static_only_files,
|
||||
do:
|
||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
|
||||
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
|
||||
)
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,32 +10,70 @@ defmodule Pleroma.CounterCache do
|
|||
import Ecto.Query
|
||||
|
||||
schema "counter_cache" do
|
||||
field(:name, :string)
|
||||
field(:count, :integer)
|
||||
field(:instance, :string)
|
||||
field(:public, :integer)
|
||||
field(:unlisted, :integer)
|
||||
field(:private, :integer)
|
||||
field(:direct, :integer)
|
||||
end
|
||||
|
||||
def changeset(struct, params) do
|
||||
struct
|
||||
|> cast(params, [:name, :count])
|
||||
|> validate_required([:name])
|
||||
|> unique_constraint(:name)
|
||||
|> cast(params, [:instance, :public, :unlisted, :private, :direct])
|
||||
|> validate_required([:instance])
|
||||
|> unique_constraint(:instance)
|
||||
end
|
||||
|
||||
def get_as_map(names) when is_list(names) do
|
||||
def get_by_instance(instance) do
|
||||
CounterCache
|
||||
|> where([cc], cc.name in ^names)
|
||||
|> Repo.all()
|
||||
|> Enum.group_by(& &1.name, & &1.count)
|
||||
|> Map.new(fn {k, v} -> {k, hd(v)} end)
|
||||
|> select([c], %{
|
||||
"public" => c.public,
|
||||
"unlisted" => c.unlisted,
|
||||
"private" => c.private,
|
||||
"direct" => c.direct
|
||||
})
|
||||
|> where([c], c.instance == ^instance)
|
||||
|> Repo.one()
|
||||
|> case do
|
||||
nil -> %{"public" => 0, "unlisted" => 0, "private" => 0, "direct" => 0}
|
||||
val -> val
|
||||
end
|
||||
end
|
||||
|
||||
def set(name, count) do
|
||||
def get_sum do
|
||||
CounterCache
|
||||
|> select([c], %{
|
||||
"public" => type(sum(c.public), :integer),
|
||||
"unlisted" => type(sum(c.unlisted), :integer),
|
||||
"private" => type(sum(c.private), :integer),
|
||||
"direct" => type(sum(c.direct), :integer)
|
||||
})
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def set(instance, values) do
|
||||
params =
|
||||
Enum.reduce(
|
||||
["public", "private", "unlisted", "direct"],
|
||||
%{"instance" => instance},
|
||||
fn param, acc ->
|
||||
Map.put_new(acc, param, Map.get(values, param, 0))
|
||||
end
|
||||
)
|
||||
|
||||
%CounterCache{}
|
||||
|> changeset(%{"name" => name, "count" => count})
|
||||
|> changeset(params)
|
||||
|> Repo.insert(
|
||||
on_conflict: [set: [count: count]],
|
||||
on_conflict: [
|
||||
set: [
|
||||
public: params["public"],
|
||||
private: params["private"],
|
||||
unlisted: params["unlisted"],
|
||||
direct: params["direct"]
|
||||
]
|
||||
],
|
||||
returning: true,
|
||||
conflict_target: :name
|
||||
conflict_target: :instance
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Pleroma.Emoji.Pack do
|
||||
@derive {Jason.Encoder, only: [:files, :pack]}
|
||||
@derive {Jason.Encoder, only: [:files, :pack, :files_count]}
|
||||
defstruct files: %{},
|
||||
files_count: 0,
|
||||
pack_file: nil,
|
||||
path: nil,
|
||||
pack: %{},
|
||||
|
@ -8,6 +9,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
@type t() :: %__MODULE__{
|
||||
files: %{String.t() => Path.t()},
|
||||
files_count: non_neg_integer(),
|
||||
pack_file: Path.t(),
|
||||
path: Path.t(),
|
||||
pack: map(),
|
||||
|
@ -16,7 +18,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
alias Pleroma.Emoji
|
||||
|
||||
@spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
|
||||
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def create(name) do
|
||||
with :ok <- validate_not_empty([name]),
|
||||
dir <- Path.join(emoji_path(), name),
|
||||
|
@ -26,10 +28,28 @@ def create(name) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec show(String.t()) :: {:ok, t()} | {:error, atom()}
|
||||
def show(name) do
|
||||
defp paginate(entities, 1, page_size), do: Enum.take(entities, page_size)
|
||||
|
||||
defp paginate(entities, page, page_size) do
|
||||
entities
|
||||
|> Enum.chunk_every(page_size)
|
||||
|> Enum.at(page - 1)
|
||||
end
|
||||
|
||||
@spec show(keyword()) :: {:ok, t()} | {:error, atom()}
|
||||
def show(opts) do
|
||||
name = opts[:name]
|
||||
|
||||
with :ok <- validate_not_empty([name]),
|
||||
{:ok, pack} <- load_pack(name) do
|
||||
shortcodes =
|
||||
pack.files
|
||||
|> Map.keys()
|
||||
|> Enum.sort()
|
||||
|> paginate(opts[:page], opts[:page_size])
|
||||
|
||||
pack = Map.put(pack, :files, Map.take(pack.files, shortcodes))
|
||||
|
||||
{:ok, validate_pack(pack)}
|
||||
end
|
||||
end
|
||||
|
@ -120,10 +140,10 @@ def list_remote(url) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec list_local() :: {:ok, map()}
|
||||
def list_local do
|
||||
@spec list_local(keyword()) :: {:ok, map(), non_neg_integer()}
|
||||
def list_local(opts) do
|
||||
with {:ok, results} <- list_packs_dir() do
|
||||
packs =
|
||||
all_packs =
|
||||
results
|
||||
|> Enum.map(fn name ->
|
||||
case load_pack(name) do
|
||||
|
@ -132,9 +152,13 @@ def list_local do
|
|||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
packs =
|
||||
all_packs
|
||||
|> paginate(opts[:page], opts[:page_size])
|
||||
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
|
||||
|
||||
{:ok, packs}
|
||||
{:ok, packs, length(all_packs)}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -146,7 +170,7 @@ def get_archive(name) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
|
||||
@spec download(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, atom()}
|
||||
def download(name, url, as) do
|
||||
uri = url |> String.trim() |> URI.parse()
|
||||
|
||||
|
@ -197,7 +221,12 @@ def load_pack(name) do
|
|||
|> Map.put(:path, Path.dirname(pack_file))
|
||||
|> Map.put(:name, name)
|
||||
|
||||
{:ok, pack}
|
||||
files_count =
|
||||
pack.files
|
||||
|> Map.keys()
|
||||
|> length()
|
||||
|
||||
{:ok, Map.put(pack, :files_count, files_count)}
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
@ -296,7 +325,9 @@ defp downloadable?(pack) do
|
|||
# Otherwise, they'd have to download it from external-src
|
||||
pack.pack["share-files"] &&
|
||||
Enum.all?(pack.files, fn {_, file} ->
|
||||
File.exists?(Path.join(pack.path, file))
|
||||
pack.path
|
||||
|> Path.join(file)
|
||||
|> File.exists?()
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -440,7 +471,7 @@ defp list_packs_dir do
|
|||
# with the API so it should be sufficient
|
||||
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
||||
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
||||
{:ok, results}
|
||||
{:ok, Enum.sort(results)}
|
||||
else
|
||||
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
|
||||
{:ls, {:error, e}} -> {:error, :ls, e}
|
||||
|
|
|
@ -124,6 +124,7 @@ def get_follow_requests(%User{id: id}) do
|
|||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> where([r], r.state == ^:follow_pending)
|
||||
|> where([r], r.following_id == ^id)
|
||||
|> where([r, f], f.deactivated != true)
|
||||
|> select([r, f], f)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
@ -141,6 +142,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)
|
||||
|
|
|
@ -17,14 +17,6 @@ def append_uri_params(uri, appended_params) do
|
|||
|> URI.to_string()
|
||||
end
|
||||
|
||||
def append_param_if_present(%{} = params, param_name, param_value) do
|
||||
if param_value do
|
||||
Map.put(params, param_name, param_value)
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
|
||||
def maybe_add_base(uri, _base), do: uri
|
||||
end
|
||||
|
|
|
@ -22,22 +22,7 @@ def options(connection_opts \\ [], %URI{} = uri) do
|
|||
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
|
||||
end
|
||||
|
||||
defp add_scheme_opts(opts, %URI{scheme: "http"}), do: opts
|
||||
|
||||
defp add_scheme_opts(opts, %URI{scheme: "https", host: host}) do
|
||||
ssl_opts = [
|
||||
ssl_options: [
|
||||
# Workaround for remote server certificate chain issues
|
||||
partial_chain: &:hackney_connect.partial_chain/1,
|
||||
|
||||
# We don't support TLS v1.3 yet
|
||||
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
|
||||
server_name_indication: to_charlist(host)
|
||||
]
|
||||
]
|
||||
|
||||
Keyword.merge(opts, ssl_opts)
|
||||
end
|
||||
defp add_scheme_opts(opts, _), do: opts
|
||||
|
||||
def after_request(_), do: :ok
|
||||
end
|
||||
|
|
22
lib/pleroma/http/ex_aws.ex
Normal file
22
lib/pleroma/http/ex_aws.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.ExAws do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour ExAws.Request.HttpClient
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
@impl true
|
||||
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
|
||||
case HTTP.request(method, url, body, headers, http_opts) do
|
||||
{:ok, env} ->
|
||||
{:ok, %{status_code: env.status, headers: env.headers, body: env.body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %{reason: reason}}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.HTTP do
|
|||
require Logger
|
||||
|
||||
@type t :: __MODULE__
|
||||
@type method() :: :get | :post | :put | :delete | :head
|
||||
|
||||
@doc """
|
||||
Performs GET request.
|
||||
|
@ -28,6 +29,9 @@ def get(url, headers \\ [], options \\ [])
|
|||
def get(nil, _, _), do: nil
|
||||
def get(url, headers, options), do: request(:get, url, "", headers, options)
|
||||
|
||||
@spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
||||
def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options)
|
||||
|
||||
@doc """
|
||||
Performs POST request.
|
||||
|
||||
|
@ -42,7 +46,7 @@ def post(url, body, headers \\ [], options \\ []),
|
|||
Builds and performs http request.
|
||||
|
||||
# Arguments:
|
||||
`method` - :get, :post, :put, :delete
|
||||
`method` - :get, :post, :put, :delete, :head
|
||||
`url` - full url
|
||||
`body` - request body
|
||||
`headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
|
||||
|
@ -52,7 +56,7 @@ def post(url, body, headers \\ [], options \\ []),
|
|||
`{:ok, %Tesla.Env{}}` or `{:error, error}`
|
||||
|
||||
"""
|
||||
@spec request(atom(), Request.url(), String.t(), Request.headers(), keyword()) ::
|
||||
@spec request(method(), Request.url(), String.t(), Request.headers(), keyword()) ::
|
||||
{:ok, Env.t()} | {:error, any()}
|
||||
def request(method, url, body, headers, options) when is_binary(url) do
|
||||
uri = URI.parse(url)
|
||||
|
|
25
lib/pleroma/http/tzdata.ex
Normal file
25
lib/pleroma/http/tzdata.ex
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Tzdata do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour Tzdata.HTTPClient
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
@impl true
|
||||
def get(url, headers, options) do
|
||||
with {:ok, %Tesla.Env{} = env} <- HTTP.get(url, headers, options) do
|
||||
{:ok, {env.status, env.headers, env.body}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def head(url, headers, options) do
|
||||
with {:ok, %Tesla.Env{} = env} <- HTTP.head(url, headers, options) do
|
||||
{:ok, {env.status, env.headers}}
|
||||
end
|
||||
end
|
||||
end
|
37
lib/pleroma/maintenance.ex
Normal file
37
lib/pleroma/maintenance.ex
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Maintenance do
|
||||
alias Pleroma.Repo
|
||||
require Logger
|
||||
|
||||
def vacuum(args) do
|
||||
case args do
|
||||
"analyze" ->
|
||||
Logger.info("Runnning VACUUM ANALYZE.")
|
||||
|
||||
Repo.query!(
|
||||
"vacuum analyze;",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
"full" ->
|
||||
Logger.info("Runnning VACUUM FULL.")
|
||||
|
||||
Logger.warn(
|
||||
"Re-packing your entire database may take a while and will consume extra disk space during the process."
|
||||
)
|
||||
|
||||
Repo.query!(
|
||||
"vacuum full;",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
_ ->
|
||||
Logger.error("Error: invalid vacuum argument.")
|
||||
end
|
||||
end
|
||||
end
|
15
lib/pleroma/maps.ex
Normal file
15
lib/pleroma/maps.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Maps do
|
||||
def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(map) do
|
||||
with false <- is_nil(key),
|
||||
false <- is_nil(value),
|
||||
{:ok, new_value} <- value_function.(value) do
|
||||
Map.put(map, key, new_value)
|
||||
else
|
||||
_ -> map
|
||||
end
|
||||
end
|
||||
end
|
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,
|
||||
:from_followers,
|
||||
:from_following,
|
||||
:from_strangers,
|
||||
|
@ -474,6 +565,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?(
|
||||
:from_followers,
|
||||
%Activity{} = activity,
|
||||
|
@ -516,4 +613,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
|
||||
|
||||
|
@ -64,6 +64,12 @@ def fetch_paginated(query, params, :offset, table_binding) do
|
|||
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
|
||||
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
|
||||
|
||||
def paginate(list, options, _method, _table_binding) when is_list(list) do
|
||||
offset = options[:offset] || 0
|
||||
limit = options[:limit] || 0
|
||||
Enum.slice(list, offset, limit)
|
||||
end
|
||||
|
||||
def paginate(query, options, :keyset, table_binding) do
|
||||
query
|
||||
|> restrict(:min_id, options, table_binding)
|
||||
|
@ -90,12 +96,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
|
||||
|
|
|
@ -31,7 +31,7 @@ defp headers do
|
|||
{"x-content-type-options", "nosniff"},
|
||||
{"referrer-policy", referrer_policy},
|
||||
{"x-download-options", "noopen"},
|
||||
{"content-security-policy", csp_string() <> ";"}
|
||||
{"content-security-policy", csp_string()}
|
||||
]
|
||||
|
||||
if report_uri do
|
||||
|
@ -43,23 +43,46 @@ defp headers do
|
|||
]
|
||||
}
|
||||
|
||||
headers ++ [{"reply-to", Jason.encode!(report_group)}]
|
||||
[{"reply-to", Jason.encode!(report_group)} | headers]
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
static_csp_rules = [
|
||||
"default-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"font-src 'self'",
|
||||
"manifest-src 'self'"
|
||||
]
|
||||
|
||||
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
|
||||
|
||||
defp csp_string do
|
||||
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
|
||||
static_url = Pleroma.Web.Endpoint.static_url()
|
||||
websocket_url = Pleroma.Web.Endpoint.websocket_url()
|
||||
report_uri = Config.get([:http_security, :report_uri])
|
||||
|
||||
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
|
||||
img_src = "img-src 'self' data: blob:"
|
||||
media_src = "media-src 'self'"
|
||||
|
||||
{img_src, media_src} =
|
||||
if Config.get([:media_proxy, :enabled]) &&
|
||||
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||
sources = get_proxy_and_attachment_sources()
|
||||
{[img_src, sources], [media_src, sources]}
|
||||
else
|
||||
{[img_src, " https:"], [media_src, " https:"]}
|
||||
end
|
||||
|
||||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||
|
||||
connect_src =
|
||||
if Pleroma.Config.get(:env) == :dev do
|
||||
connect_src <> " http://localhost:3035/"
|
||||
[connect_src, " http://localhost:3035/"]
|
||||
else
|
||||
connect_src
|
||||
end
|
||||
|
@ -71,27 +94,51 @@ defp csp_string do
|
|||
"script-src 'self'"
|
||||
end
|
||||
|
||||
main_part = [
|
||||
"default-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"media-src 'self' https:",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"font-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
connect_src,
|
||||
script_src
|
||||
]
|
||||
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
|
||||
insecure = if scheme == "https", do: "upgrade-insecure-requests"
|
||||
|
||||
report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
|
||||
|
||||
insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
|
||||
|
||||
(main_part ++ report ++ insecure)
|
||||
|> Enum.join("; ")
|
||||
@csp_start
|
||||
|> add_csp_param(img_src)
|
||||
|> add_csp_param(media_src)
|
||||
|> add_csp_param(connect_src)
|
||||
|> add_csp_param(script_src)
|
||||
|> add_csp_param(insecure)
|
||||
|> add_csp_param(report)
|
||||
|> :erlang.iolist_to_binary()
|
||||
end
|
||||
|
||||
defp get_proxy_and_attachment_sources do
|
||||
media_proxy_whitelist =
|
||||
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
|
||||
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
|
||||
|
||||
s3_endpoint =
|
||||
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
|
||||
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)
|
||||
end
|
||||
|
||||
defp add_source(iodata, nil), do: iodata
|
||||
defp add_source(iodata, source), do: [[?\s, source] | iodata]
|
||||
|
||||
defp add_csp_param(csp_iodata, nil), do: csp_iodata
|
||||
|
||||
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
|
||||
|
||||
def warn_if_disabled do
|
||||
unless Config.get([:http_security, :enabled]) do
|
||||
Logger.warn("
|
||||
|
|
|
@ -10,6 +10,8 @@ defmodule Pleroma.Plugs.UploadedMedia do
|
|||
import Pleroma.Web.Gettext
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
@behaviour Plug
|
||||
# no slashes
|
||||
@path "media"
|
||||
|
@ -35,8 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
|
|||
%{query_params: %{"name" => name}} = conn ->
|
||||
name = String.replace(name, "\"", "\\\"")
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
|
||||
put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
|
||||
|
||||
conn ->
|
||||
conn
|
||||
|
@ -47,7 +48,8 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
|
|||
|
||||
with uploader <- Keyword.fetch!(config, :uploader),
|
||||
proxy_remote = Keyword.get(config, :proxy_remote, false),
|
||||
{:ok, get_method} <- uploader.get_file(file) do
|
||||
{:ok, get_method} <- uploader.get_file(file),
|
||||
false <- media_is_banned(conn, get_method) do
|
||||
get_media(conn, get_method, proxy_remote, opts)
|
||||
else
|
||||
_ ->
|
||||
|
@ -59,6 +61,14 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
|
|||
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp media_is_banned(%{request_path: path} = _conn, {:static_dir, _}) do
|
||||
MediaProxy.in_banned_urls(Pleroma.Web.base_url() <> path)
|
||||
end
|
||||
|
||||
defp media_is_banned(_, {:url, url}), do: MediaProxy.in_banned_urls(url)
|
||||
|
||||
defp media_is_banned(_, _), do: false
|
||||
|
||||
defp get_media(conn, {:static_dir, directory}, _, opts) do
|
||||
static_opts =
|
||||
Map.get(opts, :static_plug_opts)
|
||||
|
|
|
@ -8,11 +8,10 @@ defmodule Pleroma.Repo do
|
|||
adapter: Ecto.Adapters.Postgres,
|
||||
migration_timestamps: [type: :naive_datetime_usec]
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
defmodule Instrumenter do
|
||||
use Prometheus.EctoInstrumenter
|
||||
end
|
||||
defmodule Instrumenter, do: use(Prometheus.EctoInstrumenter)
|
||||
|
||||
@doc """
|
||||
Dynamically loads the repository url from the
|
||||
|
@ -50,36 +49,30 @@ def get_assoc(resource, association) do
|
|||
end
|
||||
end
|
||||
|
||||
def check_migrations_applied!() do
|
||||
unless Pleroma.Config.get(
|
||||
[:i_am_aware_this_may_cause_data_loss, :disable_migration_check],
|
||||
false
|
||||
) do
|
||||
Ecto.Migrator.with_repo(__MODULE__, fn repo ->
|
||||
down_migrations =
|
||||
Ecto.Migrator.migrations(repo)
|
||||
|> Enum.reject(fn
|
||||
{:up, _, _} -> true
|
||||
{:down, _, _} -> false
|
||||
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}
|
||||
|
||||
if length(down_migrations) > 0 do
|
||||
down_migrations_text =
|
||||
Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end)
|
||||
|
||||
Logger.error(
|
||||
"The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true"
|
||||
records ->
|
||||
last_id = List.last(records).id
|
||||
{records, last_id}
|
||||
end
|
||||
end,
|
||||
fn _ -> :ok end
|
||||
)
|
||||
|
||||
raise Pleroma.Repo.UnappliedMigrationsError
|
||||
end
|
||||
end)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Pleroma.Repo.UnappliedMigrationsError do
|
||||
defexception message: "Unapplied Migrations detected"
|
||||
end
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -97,20 +97,11 @@ def calculate_stat_data do
|
|||
}
|
||||
end
|
||||
|
||||
def get_status_visibility_count do
|
||||
counter_cache =
|
||||
CounterCache.get_as_map([
|
||||
"status_visibility_public",
|
||||
"status_visibility_private",
|
||||
"status_visibility_unlisted",
|
||||
"status_visibility_direct"
|
||||
])
|
||||
|
||||
%{
|
||||
public: counter_cache["status_visibility_public"] || 0,
|
||||
unlisted: counter_cache["status_visibility_unlisted"] || 0,
|
||||
private: counter_cache["status_visibility_private"] || 0,
|
||||
direct: counter_cache["status_visibility_direct"] || 0
|
||||
}
|
||||
def get_status_visibility_count(instance \\ nil) do
|
||||
if is_nil(instance) do
|
||||
CounterCache.get_sum()
|
||||
else
|
||||
CounterCache.get_by_instance(instance)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
@ -262,37 +263,60 @@ def account_status(%User{deactivated: true}), do: :deactivated
|
|||
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
|
||||
|
||||
def account_status(%User{confirmation_pending: true}) do
|
||||
case Config.get([:instance, :account_activation_required]) do
|
||||
true -> :confirmation_pending
|
||||
_ -> :active
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
:confirmation_pending
|
||||
else
|
||||
:active
|
||||
end
|
||||
end
|
||||
|
||||
def account_status(%User{}), do: :active
|
||||
|
||||
@spec visible_for?(User.t(), User.t() | nil) :: boolean()
|
||||
def visible_for?(user, for_user \\ nil)
|
||||
@spec visible_for(User.t(), User.t() | nil) ::
|
||||
:visible
|
||||
| :invisible
|
||||
| :restricted_unauthenticated
|
||||
| :deactivated
|
||||
| :confirmation_pending
|
||||
def visible_for(user, for_user \\ nil)
|
||||
|
||||
def visible_for?(%User{invisible: true}, _), do: false
|
||||
def visible_for(%User{invisible: true}, _), do: :invisible
|
||||
|
||||
def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
|
||||
def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
|
||||
|
||||
def visible_for?(%User{local: local} = user, nil) do
|
||||
cfg_key =
|
||||
if local,
|
||||
do: :local,
|
||||
else: :remote
|
||||
|
||||
if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
|
||||
do: false,
|
||||
else: account_status(user) == :active
|
||||
def visible_for(%User{} = user, nil) do
|
||||
if restrict_unauthenticated?(user) do
|
||||
:restrict_unauthenticated
|
||||
else
|
||||
visible_account_status(user)
|
||||
end
|
||||
end
|
||||
|
||||
def visible_for?(%User{} = user, for_user) do
|
||||
account_status(user) == :active || superuser?(for_user)
|
||||
def visible_for(%User{} = user, for_user) do
|
||||
if superuser?(for_user) do
|
||||
:visible
|
||||
else
|
||||
visible_account_status(user)
|
||||
end
|
||||
end
|
||||
|
||||
def visible_for?(_, _), do: false
|
||||
def visible_for(_, _), do: :invisible
|
||||
|
||||
defp restrict_unauthenticated?(%User{local: local}) do
|
||||
config_key = if local, do: :local, else: :remote
|
||||
|
||||
Config.get([:restrict_unauthenticated, :profiles, config_key], false)
|
||||
end
|
||||
|
||||
defp visible_account_status(user) do
|
||||
status = account_status(user)
|
||||
|
||||
if status in [:active, :password_reset_pending] do
|
||||
:visible
|
||||
else
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
@spec superuser?(User.t()) :: boolean()
|
||||
def superuser?(%User{local: true, is_admin: true}), do: true
|
||||
|
@ -432,6 +456,7 @@ def update_changeset(struct, params \\ %{}) do
|
|||
params,
|
||||
[
|
||||
:bio,
|
||||
:raw_bio,
|
||||
:name,
|
||||
:emoji,
|
||||
:avatar,
|
||||
|
@ -463,6 +488,7 @@ def update_changeset(struct, params \\ %{}) do
|
|||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, min: 1, max: name_limit)
|
||||
|> validate_inclusion(:actor_type, ["Person", "Service"])
|
||||
|> put_fields()
|
||||
|> put_emoji()
|
||||
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|
||||
|
@ -538,9 +564,10 @@ def update_as_admin_changeset(struct, params) do
|
|||
|> delete_change(:also_known_as)
|
||||
|> unique_constraint(:email)
|
||||
|> validate_format(:email, @email_regex)
|
||||
|> validate_inclusion(:actor_type, ["Person", "Service"])
|
||||
end
|
||||
|
||||
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
|
||||
def update_as_admin(user, params) do
|
||||
params = Map.put(params, "password_confirmation", params["password"])
|
||||
changeset = update_as_admin_changeset(user, params)
|
||||
|
@ -561,7 +588,7 @@ def password_update_changeset(struct, params) do
|
|||
|> put_change(:password_reset_pending, false)
|
||||
end
|
||||
|
||||
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
@spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
|
||||
def reset_password(%User{} = user, params) do
|
||||
reset_password(user, user, params)
|
||||
end
|
||||
|
@ -606,7 +633,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)
|
||||
|
@ -746,7 +782,6 @@ def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
|
|||
|
||||
follower
|
||||
|> update_following_count()
|
||||
|> set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -775,7 +810,6 @@ defp do_unfollow(%User{} = follower, %User{} = followed) do
|
|||
{:ok, follower} =
|
||||
follower
|
||||
|> update_following_count()
|
||||
|> set_cache()
|
||||
|
||||
{:ok, follower, followed}
|
||||
|
||||
|
@ -1127,35 +1161,25 @@ defp follow_information_changeset(user, params) do
|
|||
])
|
||||
end
|
||||
|
||||
@spec update_follower_count(User.t()) :: {:ok, User.t()}
|
||||
def update_follower_count(%User{} = user) do
|
||||
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
follower_count_query =
|
||||
User.Query.build(%{followers: user, deactivated: false})
|
||||
|> select([u], %{count: count(u.id)})
|
||||
follower_count = FollowingRelationship.follower_count(user)
|
||||
|
||||
User
|
||||
|> where(id: ^user.id)
|
||||
|> join(:inner, [u], s in subquery(follower_count_query))
|
||||
|> update([u, s],
|
||||
set: [follower_count: s.count]
|
||||
)
|
||||
|> select([u], u)
|
||||
|> Repo.update_all([])
|
||||
|> case do
|
||||
{1, [user]} -> set_cache(user)
|
||||
_ -> {:error, user}
|
||||
end
|
||||
user
|
||||
|> follow_information_changeset(%{follower_count: follower_count})
|
||||
|> update_and_set_cache
|
||||
else
|
||||
{:ok, maybe_fetch_follow_information(user)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_following_count(User.t()) :: User.t()
|
||||
@spec update_following_count(User.t()) :: {:ok, User.t()}
|
||||
def update_following_count(%User{local: false} = user) do
|
||||
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
maybe_fetch_follow_information(user)
|
||||
{:ok, maybe_fetch_follow_information(user)}
|
||||
else
|
||||
user
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1164,7 +1188,7 @@ def update_following_count(%User{local: true} = user) do
|
|||
|
||||
user
|
||||
|> follow_information_changeset(%{following_count: following_count})
|
||||
|> Repo.update!()
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def set_unread_conversation_count(%User{local: true} = user) do
|
||||
|
@ -1487,6 +1511,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
|
||||
|
@ -1573,6 +1600,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()
|
||||
|
@ -1610,6 +1644,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
|
||||
|
|
|
@ -45,7 +45,7 @@ defmodule Pleroma.User.Query do
|
|||
is_admin: boolean(),
|
||||
is_moderator: boolean(),
|
||||
super_users: boolean(),
|
||||
exclude_service_users: boolean(),
|
||||
invisible: boolean(),
|
||||
followers: User.t(),
|
||||
friends: User.t(),
|
||||
recipients_from_activity: [String.t()],
|
||||
|
@ -89,8 +89,8 @@ defp compose_query({key, value}, query)
|
|||
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
|
||||
end
|
||||
|
||||
defp compose_query({:exclude_service_users, _}, query) do
|
||||
where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
|
||||
defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
|
||||
where(query, [u], u.invisible == ^bool)
|
||||
end
|
||||
|
||||
defp compose_query({key, value}, query)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.ControllerHelper
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.FederatingPlug
|
||||
alias Pleroma.Web.Federator
|
||||
|
@ -230,27 +231,23 @@ def outbox(
|
|||
when page? in [true, "true"] do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- User.ensure_keys_present(user) do
|
||||
activities =
|
||||
if params["max_id"] do
|
||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
||||
"max_id" => params["max_id"],
|
||||
# This is a hack because postgres generates inefficient queries when filtering by
|
||||
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
|
||||
"include_poll_votes" => true,
|
||||
"limit" => 10
|
||||
})
|
||||
else
|
||||
ActivityPub.fetch_user_activities(user, for_user, %{
|
||||
"limit" => 10,
|
||||
"include_poll_votes" => true
|
||||
})
|
||||
end
|
||||
# "include_poll_votes" is a hack because postgres generates inefficient
|
||||
# queries when filtering by 'Answer', poll votes will be hidden by the
|
||||
# visibility filter in this case anyway
|
||||
params =
|
||||
params
|
||||
|> Map.drop(["nickname", "page"])
|
||||
|> Map.put("include_poll_votes", true)
|
||||
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, for_user, params)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("activity_collection_page.json", %{
|
||||
activities: activities,
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||
iri: "#{user.ap_id}/outbox"
|
||||
})
|
||||
end
|
||||
|
@ -353,21 +350,24 @@ def read_inbox(
|
|||
%{"nickname" => nickname, "page" => page?} = params
|
||||
)
|
||||
when page? in [true, "true"] do
|
||||
params =
|
||||
params
|
||||
|> Map.drop(["nickname", "page"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
||||
|
||||
activities =
|
||||
if params["max_id"] do
|
||||
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
|
||||
"max_id" => params["max_id"],
|
||||
"limit" => 10
|
||||
})
|
||||
else
|
||||
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
|
||||
end
|
||||
[user.ap_id | User.following(user)]
|
||||
|> ActivityPub.fetch_activities(params)
|
||||
|> Enum.reverse()
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("activity_collection_page.json", %{
|
||||
activities: activities,
|
||||
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
||||
iri: "#{user.ap_id}/inbox"
|
||||
})
|
||||
end
|
||||
|
@ -514,7 +514,6 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
|||
{new_user, for_user}
|
||||
end
|
||||
|
||||
# TODO: Add support for "object" field
|
||||
@doc """
|
||||
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
|
||||
|
||||
|
@ -525,6 +524,8 @@ defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
|||
Response:
|
||||
- HTTP Code: 201 Created
|
||||
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
|
||||
|
||||
Note: Will not point to a URL with a `Location` header because no standalone Activity has been created.
|
||||
"""
|
||||
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
|
|
|
@ -5,8 +5,10 @@ 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
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
|
@ -64,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,
|
||||
|
@ -85,15 +123,35 @@ def like(actor, object) do
|
|||
end
|
||||
end
|
||||
|
||||
# Retricted to user updates for now, always public
|
||||
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
|
||||
def update(actor, object) do
|
||||
to = [Pleroma.Constants.as_public(), actor.follower_address]
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"type" => "Update",
|
||||
"actor" => actor.ap_id,
|
||||
"object" => object,
|
||||
"to" => to
|
||||
}, []}
|
||||
end
|
||||
|
||||
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
|
||||
def announce(actor, object, options \\ []) do
|
||||
public? = Keyword.get(options, :public, false)
|
||||
to = [actor.follower_address, object.data["actor"]]
|
||||
|
||||
to =
|
||||
if public? do
|
||||
[Pleroma.Constants.as_public() | to]
|
||||
else
|
||||
to
|
||||
cond do
|
||||
actor.ap_id == Relay.relay_ap_id() ->
|
||||
[actor.follower_address]
|
||||
|
||||
public? ->
|
||||
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
|
||||
|
||||
true ->
|
||||
[actor.follower_address, object.data["actor"]]
|
||||
end
|
||||
|
||||
{:ok,
|
||||
|
|
|
@ -8,18 +8,15 @@ 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
|
||||
|
||||
def filter(%{} = object), do: get_policies() |> filter(object)
|
||||
|
||||
def get_policies do
|
||||
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
|
||||
Pleroma.Config.get([:mrf, :policies], []) |> get_policies()
|
||||
end
|
||||
|
||||
defp get_policies(policy) when is_atom(policy), do: [policy]
|
||||
|
@ -54,7 +51,7 @@ def describe(policies) do
|
|||
get_policies()
|
||||
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
|
||||
|
||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
|
||||
exclusions = Pleroma.Config.get([:mrf, :transparency_exclusions])
|
||||
|
||||
base =
|
||||
%{
|
||||
|
|
|
@ -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
|
|
@ -13,8 +13,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
|
|||
|
||||
defp delist_message(message, threshold) when threshold > 0 do
|
||||
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
|
||||
to = message["to"] || []
|
||||
cc = message["cc"] || []
|
||||
|
||||
follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection)
|
||||
follower_collection? = Enum.member?(to ++ cc, follower_collection)
|
||||
|
||||
message =
|
||||
case get_recipient_count(message) do
|
||||
|
@ -71,7 +73,8 @@ defp get_recipient_count(message) do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create"} = message) do
|
||||
def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message)
|
||||
when object_type in ~w{Note Article} do
|
||||
reject_threshold =
|
||||
Pleroma.Config.get(
|
||||
[:mrf_hellthread, :reject_threshold],
|
||||
|
|
|
@ -3,21 +3,23 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
@moduledoc "Filter activities depending on their origin instance"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
defp check_accept(%{host: actor_host} = _actor_info, object) do
|
||||
accepts =
|
||||
Pleroma.Config.get([:mrf_simple, :accept])
|
||||
Config.get([:mrf_simple, :accept])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
cond do
|
||||
accepts == [] -> {:ok, object}
|
||||
actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
||||
true -> {:reject, nil}
|
||||
end
|
||||
|
@ -25,7 +27,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do
|
|||
|
||||
defp check_reject(%{host: actor_host} = _actor_info, object) do
|
||||
rejects =
|
||||
Pleroma.Config.get([:mrf_simple, :reject])
|
||||
Config.get([:mrf_simple, :reject])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(rejects, actor_host) do
|
||||
|
@ -41,7 +43,7 @@ defp check_media_removal(
|
|||
)
|
||||
when length(child_attachment) > 0 do
|
||||
media_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :media_removal])
|
||||
Config.get([:mrf_simple, :media_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
@ -65,7 +67,7 @@ defp check_media_nsfw(
|
|||
} = object
|
||||
) do
|
||||
media_nsfw =
|
||||
Pleroma.Config.get([:mrf_simple, :media_nsfw])
|
||||
Config.get([:mrf_simple, :media_nsfw])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
@ -85,7 +87,7 @@ defp check_media_nsfw(_actor_info, object), do: {:ok, object}
|
|||
|
||||
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
|
||||
timeline_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :federated_timeline_removal])
|
||||
Config.get([:mrf_simple, :federated_timeline_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
object =
|
||||
|
@ -108,7 +110,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
|
|||
|
||||
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
|
||||
report_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :report_removal])
|
||||
Config.get([:mrf_simple, :report_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(report_removal, actor_host) do
|
||||
|
@ -122,7 +124,7 @@ defp check_report_removal(_actor_info, object), do: {:ok, object}
|
|||
|
||||
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
|
||||
avatar_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :avatar_removal])
|
||||
Config.get([:mrf_simple, :avatar_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(avatar_removal, actor_host) do
|
||||
|
@ -136,7 +138,7 @@ defp check_avatar_removal(_actor_info, object), do: {:ok, object}
|
|||
|
||||
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
|
||||
banner_removal =
|
||||
Pleroma.Config.get([:mrf_simple, :banner_removal])
|
||||
Config.get([:mrf_simple, :banner_removal])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(banner_removal, actor_host) do
|
||||
|
@ -197,10 +199,10 @@ def filter(object), do: {:ok, object}
|
|||
|
||||
@impl true
|
||||
def describe do
|
||||
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
|
||||
exclusions = Config.get([:mrf, :transparency_exclusions])
|
||||
|
||||
mrf_simple =
|
||||
Pleroma.Config.get(:mrf_simple)
|
||||
Config.get(:mrf_simple)
|
||||
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
|
|
|
@ -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,18 +9,31 @@ 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
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
|
||||
|
||||
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||
def validate(object, meta)
|
||||
|
||||
def validate(%{"type" => "Update"} = update_activity, meta) do
|
||||
with {:ok, update_activity} <-
|
||||
update_activity
|
||||
|> UpdateValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
update_activity = stringify_keys(update_activity)
|
||||
{:ok, update_activity, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Undo"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|
@ -43,8 +56,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 +84,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 +106,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,13 +35,12 @@ 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: [])
|
||||
|
||||
# see if needed
|
||||
field(:conversation, :string)
|
||||
field(:context_id, :string)
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue