Merge branch 'develop' into feature/masto_api_markers

This commit is contained in:
Maksim Pechnikov 2019-10-21 11:32:28 +03:00
commit 243719a965
119 changed files with 2392 additions and 4308 deletions

View file

@ -15,6 +15,7 @@ cache:
stages:
- build
- test
- benchmark
- deploy
- release
@ -28,6 +29,36 @@ build:
- mix deps.get
- mix compile --force
docs-build:
stage: build
only:
- master@pleroma/pleroma
- develop@pleroma/pleroma
variables:
MIX_ENV: dev
PLEROMA_BUILD_ENV: prod
script:
- mix deps.get
- mix compile
- mix docs
artifacts:
paths:
- priv/static/doc
benchmark:
stage: benchmark
variables:
MIX_ENV: benchmark
services:
- name: lainsoykaf/postgres-with-rum
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- mix pleroma.load_testing
unit-testing:
stage: test
services:
@ -70,7 +101,7 @@ docs-deploy:
stage: deploy
image: alpine:latest
only:
- master@pleroma/pleroma
- stable@pleroma/pleroma
- develop@pleroma/pleroma
before_script:
- apk add curl
@ -127,9 +158,10 @@ amd64:
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0
only: &release-only
- master@pleroma/pleroma
- stable@pleroma/pleroma
- develop@pleroma/pleroma
- /^maint/.*$/@pleroma/pleroma
- /^release/.*$/@pleroma/pleroma
artifacts: &release-artifacts
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
paths:

View file

@ -4,8 +4,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
- **Breaking**: OStatus protocol support
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Enabled `:instance, extended_nickname_format` in the default config
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
- Extract RSS functionality from OStatus
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
<details>
<summary>API Changes</summary>
- **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
- Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
</details>
### Added
- Refreshing poll results for remote polls
- Authentication: Added rate limit for password-authorized actions / login existence checks
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
<details>
<summary>API Changes</summary>
- Job queue stats to the healthcheck page
- Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
@ -14,9 +45,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
- Authentication: Added rate limit for password-authorized actions / login existence checks
- Metadata Link: Atom syndication Feed
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body).
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@ -29,14 +63,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
- OStatus: Extract RSS functionality
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
</details>
### Fixed
- Report emails now include functional links to profiles of remote user accounts
<details>
<summary>API Changes</summary>
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- Added `:instance, extended_nickname_format` setting to the default config
- Report emails now include functional links to profiles of remote user accounts
</details>
## [1.1.0] - 2019-??-??
## [1.1.2] - 2019-10-18
### Fixed
- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.
## [1.1.1] - 2019-10-18
### Fixed
- One of the migrations between 1.0.0 and 1.1.0 wiping user info of the relay user because of unexpected behavior of postgresql's `jsonb_set`, resulting in inability to post in the default configuration. If you were affected, please run the following query in postgres console, the relay user will be recreated automatically:
```
delete from users where ap_id = 'https://your.instance.hostname/relay';
```
- Bad user search matches
## [1.1.0] - 2019-10-14
**Breaking:** The stable branch has been changed from `master` to `stable`. If you want to keep using 1.0, the `release/1.0` branch will receive security updates for 6 months after 1.1 release.
**OTP Note:** `pleroma_ctl` in 1.0 defaults to `master` and doesn't support specifying arbitrary branches, making `./pleroma_ctl update` fail. To fix this, fetch a version of `pleroma_ctl` from 1.1 using the command below and proceed with the update normally:
```
curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/develop/rel/files/bin/pleroma_ctl'
```
### Security
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
@ -44,16 +101,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** GNU Social API with Qvitter extensions support
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
- ActivityPub: The `/objects/:uuid/likes` endpoint.
### Changed
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Configuration: added `config/description.exs`, from which `docs/config.md` is generated
- Configuration: OpenGraph and TwitterCard providers enabled by default
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Mastodon API: `pleroma.thread_muted` key in the Status entity
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
- NodeInfo: Return `mailerEnabled` in `metadata`
@ -62,7 +118,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
- The `Pleroma.FlakeId` module has been replaced with the `flake_id` library.
### Fixed
- Following from Osada
@ -73,21 +129,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Misskey's endless polls being unable to render
- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
- Mastodon API: Notifications endpoint crashing if one notification failed to render
- Mastodon API: `exclude_replies` is correctly handled again.
- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
- Existing user id not being preserved on insert conflict
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Ensure the `account` field is not empty when rendering Notification entities.
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
- Rich Media: Parser failing when no TTL can be found by image TTL setters
- Rich Media: The crawled URL is now spliced into the rich media data.
- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
- Report email not being sent to admins when the reporter is a remote user
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
- ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
- Mastodon API: `exclude_replies` is correctly handled again.
- ActivityPub: Correct addressing of Undo.
- ActivityPub: Correct addressing of profile update activities.
- ActivityPub: Polls are now refreshed when necessary.
- Report emails now include functional links to profiles of remote user accounts
- Existing user id not being preserved on insert conflict
- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
- Report email not being sent to admins when the reporter is a remote user
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
### Added
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
@ -101,6 +164,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
- Mastodon API, extension: Ability to reset avatar, profile banner, and background
- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
- Mastodon API: Add support for muting/unmuting notifications
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
@ -109,7 +173,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
- Mastodon API: Improve support for the user profile custom fields
- Mastodon API: follower/following counters are nullified when `hide_follows`/`hide_followers` and `hide_follows_count`/`hide_followers_count` are set
- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID
@ -127,11 +192,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
- Pleroma API: Email change endpoint.
- Admin API: Added moderation log
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- Web response cache (currently, enabled for ActivityPub)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
- Reverse Proxy: Do not retry failed requests to limit pressure on the peer
### Changed

View file

@ -0,0 +1,229 @@
defmodule Pleroma.LoadTesting.Fetcher do
use Pleroma.LoadTesting.Helper
def fetch_user(user) do
Benchee.run(%{
"By id" => fn -> Repo.get_by(User, id: user.id) end,
"By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end,
"By email" => fn -> Repo.get_by(User, email: user.email) end,
"By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end
})
end
def query_timelines(user) do
home_timeline_params = %{
"count" => 20,
"with_muted" => true,
"type" => ["Create", "Announce"],
"blocking_user" => user,
"muting_user" => user,
"user" => user
}
mastodon_public_timeline_params = %{
"count" => 20,
"local_only" => true,
"only_media" => "false",
"type" => ["Create", "Announce"],
"with_muted" => "true",
"blocking_user" => user,
"muting_user" => user
}
mastodon_federated_timeline_params = %{
"count" => 20,
"only_media" => "false",
"type" => ["Create", "Announce"],
"with_muted" => "true",
"blocking_user" => user,
"muting_user" => user
}
Benchee.run(%{
"User home timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
[user.ap_id | user.following],
home_timeline_params
)
end,
"User mastodon public timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_public_timeline_params
)
end,
"User mastodon federated public timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_federated_timeline_params
)
end
})
home_activities =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
[user.ap_id | user.following],
home_timeline_params
)
public_activities =
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params)
public_federated_activities =
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
mastodon_federated_timeline_params
)
Benchee.run(%{
"Rendering home timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: home_activities,
for: user,
as: :activity
})
end,
"Rendering public timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: public_activities,
for: user,
as: :activity
})
end,
"Rendering public federated timeline" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: public_federated_activities,
for: user,
as: :activity
})
end
})
end
def query_notifications(user) do
without_muted_params = %{"count" => "20", "with_muted" => "false"}
with_muted_params = %{"count" => "20", "with_muted" => "true"}
Benchee.run(%{
"Notifications without muted" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
end,
"Notifications with muted" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
end
})
without_muted_notifications =
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
with_muted_notifications =
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
Benchee.run(%{
"Render notifications without muted" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: without_muted_notifications,
for: user
})
end,
"Render notifications with muted" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: with_muted_notifications,
for: user
})
end
})
end
def query_dms(user) do
params = %{
"count" => "20",
"with_muted" => "true",
"type" => "Create",
"blocking_user" => user,
"user" => user,
visibility: "direct"
}
Benchee.run(%{
"Direct messages with muted" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(params)
end,
"Direct messages without muted" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
end
})
dms_with_muted =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(params)
dms_without_muted =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
Benchee.run(%{
"Rendering dms with muted" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: dms_with_muted,
for: user,
as: :activity
})
end,
"Rendering dms without muted" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: dms_without_muted,
for: user,
as: :activity
})
end
})
end
def query_long_thread(user, activity) do
Benchee.run(%{
"Fetch main post" => fn ->
Pleroma.Activity.get_by_id_with_object(activity.id)
end,
"Fetch context of main post" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
activity.data["context"],
%{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
}
)
end
})
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
context =
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
activity.data["context"],
%{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
}
)
Benchee.run(%{
"Render status" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{
activity: activity,
for: user
})
end,
"Render context" => fn ->
Pleroma.Web.MastodonAPI.StatusView.render(
"index.json",
for: user,
activities: context,
as: :activity
)
|> Enum.reverse()
end
})
end
end

View file

@ -0,0 +1,352 @@
defmodule Pleroma.LoadTesting.Generator do
use Pleroma.LoadTesting.Helper
alias Pleroma.Web.CommonAPI
def generate_users(opts) do
IO.puts("Starting generating #{opts[:users_max]} users...")
{time, _} = :timer.tc(fn -> do_generate_users(opts) end)
IO.puts("Inserting users take #{to_sec(time)} sec.\n")
end
defp do_generate_users(opts) do
max = Keyword.get(opts, :users_max)
Task.async_stream(
1..max,
&generate_user_data(&1),
max_concurrency: 10,
timeout: 30_000
)
|> Enum.to_list()
end
defp generate_user_data(i) do
remote = Enum.random([true, false])
user = %User{
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
password_hash:
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
bio: "Tester Number #{i}",
info: %{},
local: remote
}
user_urls =
if remote do
base_url =
Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"])
ap_id = "#{base_url}/users/#{user.nickname}"
%{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
following: [ap_id]
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(user)]
}
end
user = Map.merge(user, user_urls)
Repo.insert!(user)
end
def generate_activities(user, users) do
do_generate_activities(user, users)
end
defp do_generate_activities(user, users) do
IO.puts("Starting generating 20000 common activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..20_000,
fn _ ->
do_generate_activity([user | users])
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting common activities take #{to_sec(time)} sec.\n")
IO.puts("Starting generating 20000 activities with mentions...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..20_000,
fn _ ->
do_generate_activity_with_mention(user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n")
IO.puts("Starting generating 10000 activities with threads...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..10_000,
fn _ ->
do_generate_threads([user | users])
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n")
end
defp do_generate_activity(users) do
post = %{
"status" => "Some status without mention with random user"
}
CommonAPI.post(Enum.random(users), post)
end
defp do_generate_activity_with_mention(user, users) do
mentions_cnt = Enum.random([2, 3, 4, 5])
with_user = Enum.random([true, false])
users = Enum.shuffle(users)
mentions_users = Enum.take(users, mentions_cnt)
mentions_users = if with_user, do: [user | mentions_users], else: mentions_users
mentions_str =
Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ")
post = %{
"status" => mentions_str <> "some status with mentions random users"
}
CommonAPI.post(Enum.random(users), post)
end
defp do_generate_threads(users) do
thread_length = Enum.random([2, 3, 4, 5])
actor = Enum.random(users)
post = %{
"status" => "Start of the thread"
}
{:ok, activity} = CommonAPI.post(actor, post)
Enum.each(1..thread_length, fn _ ->
user = Enum.random(users)
post = %{
"status" => "@#{actor.nickname} reply to thread",
"in_reply_to_status_id" => activity.id
}
CommonAPI.post(user, post)
end)
end
def generate_remote_activities(user, users) do
do_generate_remote_activities(user, users)
end
defp do_generate_remote_activities(user, users) do
IO.puts("Starting generating 10000 remote activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
1..10_000,
fn i ->
do_generate_remote_activity(i, user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n")
end
defp do_generate_remote_activity(i, user, users) do
actor = Enum.random(users)
%{host: host} = URI.parse(actor.ap_id)
date = Date.utc_today()
datetime = DateTime.utc_now()
map = %{
"actor" => actor.ap_id,
"cc" => [actor.follower_address, user.ap_id],
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"id" => actor.ap_id <> "/statuses/#{i}/activity",
"object" => %{
"actor" => actor.ap_id,
"atomUri" => actor.ap_id <> "/statuses/#{i}",
"attachment" => [],
"attributedTo" => actor.ap_id,
"bcc" => [],
"bto" => [],
"cc" => [actor.follower_address, user.ap_id],
"content" =>
"<p><span class=\"h-card\"><a href=\"" <>
user.ap_id <>
"\" class=\"u-url mention\">@<span>" <> user.nickname <> "</span></a></span></p>",
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"conversation" =>
"tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
"emoji" => %{},
"id" => actor.ap_id <> "/statuses/#{i}",
"inReplyTo" => nil,
"inReplyToAtomUri" => nil,
"published" => datetime,
"sensitive" => true,
"summary" => "cw",
"tag" => [
%{
"href" => user.ap_id,
"name" => "@#{user.nickname}@#{host}",
"type" => "Mention"
}
],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Note",
"url" => "http://#{host}/@#{actor.nickname}/#{i}"
},
"published" => datetime,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create"
}
Pleroma.Web.ActivityPub.ActivityPub.insert(map, false)
end
def generate_dms(user, users, opts) do
IO.puts("Starting generating #{opts[:dms_max]} DMs")
{time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end)
IO.puts("Inserting dms take #{to_sec(time)} sec.\n")
end
defp do_generate_dms(user, users, opts) do
Task.async_stream(
1..opts[:dms_max],
fn _ ->
do_generate_dm(user, users)
end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end
defp do_generate_dm(user, users) do
post = %{
"status" => "@#{user.nickname} some direct message",
"visibility" => "direct"
}
CommonAPI.post(Enum.random(users), post)
end
def generate_long_thread(user, users, opts) do
IO.puts("Starting generating long thread with #{opts[:thread_length]} replies")
{time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end)
IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n")
{:ok, activity}
end
defp do_generate_long_thread(user, users, opts) do
{:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"})
Task.async_stream(
1..opts[:thread_length],
fn _ -> do_generate_thread(users, id) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
activity
end
defp do_generate_thread(users, activity_id) do
CommonAPI.post(Enum.random(users), %{
"status" => "reply to main post",
"in_reply_to_status_id" => activity_id
})
end
def generate_non_visible_message(user, users) do
IO.puts("Starting generating 1000 non visible posts")
{time, _} =
:timer.tc(fn ->
do_generate_non_visible_posts(user, users)
end)
IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n")
end
defp do_generate_non_visible_posts(user, users) do
[not_friend | users] = users
make_friends(user, users)
Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end
defp make_friends(_user, []), do: nil
defp make_friends(user, [friend | users]) do
{:ok, _} = User.follow(user, friend)
{:ok, _} = User.follow(friend, user)
make_friends(user, users)
end
defp do_generate_non_visible_post(not_friend, users) do
post = %{
"status" => "some non visible post",
"visibility" => "private"
}
{:ok, activity} = CommonAPI.post(not_friend, post)
thread_length = Enum.random([2, 3, 4, 5])
Enum.each(1..thread_length, fn _ ->
user = Enum.random(users)
post = %{
"status" => "@#{not_friend.nickname} reply to non visible post",
"in_reply_to_status_id" => activity.id,
"visibility" => "private"
}
CommonAPI.post(user, post)
end)
end
end

View file

@ -0,0 +1,11 @@
defmodule Pleroma.LoadTesting.Helper do
defmacro __using__(_) do
quote do
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
defp to_sec(microseconds), do: microseconds / 1_000_000
end
end
end

View file

@ -0,0 +1,134 @@
defmodule Mix.Tasks.Pleroma.LoadTesting do
use Mix.Task
use Pleroma.LoadTesting.Helper
import Mix.Pleroma
import Pleroma.LoadTesting.Generator
import Pleroma.LoadTesting.Fetcher
@shortdoc "Factory for generation data"
@moduledoc """
Generates data like:
- local/remote users
- local/remote activities with notifications
- direct messages
- long thread
- non visible posts
## Generate data
MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000
MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000
Options:
- `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u`
- `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d`
- `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t`
"""
@aliases [u: :users, d: :dms, t: :thread_length]
@switches [
users: :integer,
dms: :integer,
thread_length: :integer
]
@users_default 20_000
@dms_default 1_000
@thread_length_default 2_000
def run(args) do
start_pleroma()
Pleroma.Config.put([:instance, :skip_thread_containment], true)
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
users_max = Keyword.get(opts, :users, @users_default)
dms_max = Keyword.get(opts, :dms, @dms_default)
thread_length = Keyword.get(opts, :thread_length, @thread_length_default)
clean_tables()
opts =
Keyword.put(opts, :users_max, users_max)
|> Keyword.put(:dms_max, dms_max)
|> Keyword.put(:thread_length, thread_length)
generate_users(opts)
# main user for queries
IO.puts("Fetching local main user...")
{time, user} =
:timer.tc(fn ->
Repo.one(
from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)
)
end)
IO.puts("Fetching main user take #{to_sec(time)} sec.\n")
IO.puts("Fetching local users...")
{time, users} =
:timer.tc(fn ->
Repo.all(
from(u in User,
where: u.id != ^user.id,
where: u.local == true,
order_by: fragment("RANDOM()"),
limit: 10
)
)
end)
IO.puts("Fetching local users take #{to_sec(time)} sec.\n")
IO.puts("Fetching remote users...")
{time, remote_users} =
:timer.tc(fn ->
Repo.all(
from(u in User,
where: u.id != ^user.id,
where: u.local == false,
order_by: fragment("RANDOM()"),
limit: 10
)
)
end)
IO.puts("Fetching remote users take #{to_sec(time)} sec.\n")
generate_activities(user, users)
generate_remote_activities(user, remote_users)
generate_dms(user, users, opts)
{:ok, activity} = generate_long_thread(user, users, opts)
generate_non_visible_message(user, users)
IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}")
IO.puts("Activities in DB: #{Repo.aggregate(from(a in Pleroma.Activity), :count, :id)}")
IO.puts("Objects in DB: #{Repo.aggregate(from(o in Pleroma.Object), :count, :id)}")
IO.puts(
"Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}"
)
fetch_user(user)
query_timelines(user)
query_notifications(user)
query_dms(user)
query_long_thread(user, activity)
Pleroma.Config.put([:instance, :skip_thread_containment], false)
query_timelines(user)
end
defp clean_tables do
IO.puts("Deleting old data...\n")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
end
end

84
config/benchmark.exs Normal file
View file

@ -0,0 +1,84 @@
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4001],
url: [port: 4001],
server: true
# Disable captha for tests
config :pleroma, Pleroma.Captcha,
# It should not be enabled for automatic tests
enabled: false,
# A fake captcha service for tests
method: Pleroma.Captcha.Mock
# Print only warnings and errors during test
config :logger, level: :warn
config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload, filters: [], link_name: false
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
config :pleroma, :instance,
email: "admin@example.com",
notify_email: "noreply@example.com",
skip_thread_containment: false,
federating: false,
external_user_synchronization: false
config :pleroma, :activitypub, sign_object_fetches: false
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "pleroma_test",
hostname: System.get_env("DB_HOST") || "localhost",
pool_size: 10
# Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1
config :tesla, adapter: Tesla.Mock
config :pleroma, :rich_media,
enabled: false,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"]
config :web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma_job_queue, disabled: true
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2,
total_user_limit: 3,
enabled: false
config :pleroma, :rate_limit,
search: [{1000, 30}, {1000, 30}],
app_account_creation: {10_000, 5},
password_reset: {1000, 30}
config :pleroma, :http_security, report_uri: "https://endpoint.com"
config :pleroma, :http, send_user_agent: false
rum_enabled = System.get_env("RUM_ENABLED") == "true"
config :pleroma, :database, rum_enabled: rum_enabled
IO.puts("RUM enabled: #{rum_enabled}")
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock

View file

@ -59,10 +59,6 @@ scheduled_jobs =
_ -> []
end
scheduled_jobs =
scheduled_jobs ++
[{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
@ -243,9 +239,7 @@ config :pleroma, :instance,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher,
Pleroma.Web.Websub,
Pleroma.Web.Salmon
Pleroma.Web.ActivityPub.Publisher
],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
@ -328,6 +322,16 @@ config :pleroma, :assets,
],
default_mascot: :pleroma_fox_tan
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.png",
type: "image/png"
}
],
theme_color: "#282c37",
background_color: "#191b22"
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: true,

View file

@ -581,9 +581,7 @@ config :pleroma, :config_description, [
type: [:list, :module],
description: "List of modules for federation publishing",
suggestions: [
Pleroma.Web.ActivityPub.Publisher,
Pleroma.Web.Websub,
Pleroma.Web.Salmo
Pleroma.Web.ActivityPub.Publisher
]
},
%{
@ -1100,6 +1098,45 @@ config :pleroma, :config_description, [
}
]
},
%{
group: :pleroma,
key: :manifest,
type: :group,
description:
"This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE",
children: [
%{
key: :icons,
type: {:list, :map},
description: "Describe the icons of the app",
suggestion: [
%{
src: "/static/logo.png"
},
%{
src: "/static/icon.png",
type: "image/png"
},
%{
src: "/static/icon.ico",
sizes: "72x72 96x96 128x128 256x256"
}
]
},
%{
key: :theme_color,
type: :string,
description: "Describe the theme color of the app",
suggestions: ["#282c37", "mediumpurple"]
},
%{
key: :background_color,
type: :string,
description: "Describe the background color of the app",
suggestions: ["#191b22", "aliceblue"]
}
]
},
%{
group: :pleroma,
key: :mrf_simple,

View file

@ -1,6 +1,6 @@
import Config
config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
config :pleroma, :instance, static: "/var/lib/pleroma/static"
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"

View file

@ -47,7 +47,7 @@ Authentication is required and the user must be an admin.
}
```
## `/api/pleroma/admin/users`
## DEPRECATED `DELETE /api/pleroma/admin/users`
### Remove a user
@ -56,6 +56,15 @@ Authentication is required and the user must be an admin.
- `nickname`
- Response: Users nickname
## `DELETE /api/pleroma/admin/users`
### Remove a user
- Method `DELETE`
- Params:
- `nicknames`
- Response: Array of user nicknames
### Create a user
- Method: `POST`
@ -154,28 +163,86 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
}
```
### Add user in permission group
## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Add user to permission group
- Method: `POST`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
## `POST /api/pleroma/admin/users/permission_group/:permission_group`
### Add users to permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group
- Method: `DELETE`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status.
## `/api/pleroma/admin/users/:nickname/activation_status`
## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
### Remove users from permission group
- Params:
- `nicknames`: nicknames array
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status.
## `PATCH /api/pleroma/admin/users/activate`
### Activate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## `PATCH /api/pleroma/admin/users/deactivate`
### Deactivate user
- Params:
- `nicknames`: nicknames array
- Response:
```json
{
users: [
{
// user object
}
]
}
```
## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
### Active or deactivate a user
- Method: `PUT`
- Params:
- `nickname`
- `status` BOOLEAN field, false value means deactivation.
@ -222,6 +289,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response:
- On success: URL of the unfollowed relay
## `GET /api/pleroma/admin/relay`
### List Relays
- Params: none
- Response:
- On success: JSON array of relays
## `/api/pleroma/admin/users/invite_token`
### Create an account registration invite token

View file

@ -13,6 +13,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
## Statuses
@ -84,6 +85,12 @@ Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user
## GET `/api/v1/notifications`
Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
## POST `/api/v1/statuses`
Additional parameters can be added to the JSON body/Form data:

View file

@ -367,6 +367,13 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
## `GET /api/v1/pleroma/conversations/read`
### Marks all user's conversations as read.
* Method `POST`
* Authentication: required
* Params: None
* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy).
## `GET /api/pleroma/emoji/packs`
### Lists the custom emoji packs on the server
* Method `GET`

View file

@ -247,6 +247,35 @@ relates to mascots on the mastodon frontend
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
on MastoFE (default: `:pleroma_fox_tan`)
## :manifest
This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.
* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the
[spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it.
Example:
```elixir
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.png"
},
%{
src: "/static/icon.png",
type: "image/png"
},
%{
src: "/static/icon.ico",
sizes: "72x72 96x96 128x128 256x256"
}
]
```
* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`)
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`)
## :mrf_simple
* `media_removal`: List of instances to remove medias from
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from

View file

@ -91,7 +91,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
```shell
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* Change to the new directory:

View file

@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* Change to the new directory:

View file

@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* Change to the new directory:

View file

@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* Change to the new directory:

View file

@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```
* 新しいディレクトリに移動します。

View file

@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
```shell
pleroma$ cd ~
pleroma$ git clone -b master https://path/to/repo
pleroma$ git clone -b stable https://path/to/repo
```
* Change to the new directory:

View file

@ -96,9 +96,9 @@ rm -r ~pleroma/*
export FLAVOUR="arm64-musl"
# Clone the release build into a temporary directory and unpack it
# Replace `master` with `develop` if you want to run the develop branch
# Replace `stable` with `unstable` if you want to run the unstable branch
su pleroma -s $SHELL -lc "
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
unzip /tmp/pleroma.zip -d /tmp/
"

View file

@ -58,7 +58,7 @@ Clone the repository:
```
$ cd /home/pleroma
$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git
$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git
```
Configure Pleroma. Note that you need a domain name at this point:

View file

@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
#### Clone pleroma's directory
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
#### Postgresql
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:

View file

@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
Lataa pleroman lähdekoodi:
`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git`
`$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`
`$ cd pleroma`

View file

@ -80,7 +80,7 @@ export FLAVOUR="arm64-musl"
# Clone the release build into a temporary directory and unpack it
su pleroma -s $SHELL -lc "
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
unzip /tmp/pleroma.zip -d /tmp/
"

View file

@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Database do
Logger.info("Removing embedded objects")
Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
"update activities set data = safe_jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
)
@ -126,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.Database do
set: [
data:
fragment(
"jsonb_set(?, '{likes}', '[]'::jsonb, true)",
"safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
object.data
)
]

View file

@ -111,19 +111,21 @@ defmodule Mix.Tasks.Pleroma.Emoji do
file_list: files_to_unzip
)
IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
emoji_txt_str =
Enum.map(
files,
fn {shortcode, path} ->
emojo_path = Path.join("/emoji/#{pack_name}", path)
"#{shortcode}, #{emojo_path}"
end
)
|> Enum.join("\n")
pack_json = %{
pack: %{
"license" => pack["license"],
"homepage" => pack["homepage"],
"description" => pack["description"],
"fallback-src" => pack["src"],
"fallback-src-sha256" => pack["src_sha256"],
"share-files" => true
},
files: files
}
File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
else
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
end

View file

@ -5,7 +5,6 @@
defmodule Mix.Tasks.Pleroma.Relay do
use Mix.Task
import Mix.Pleroma
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Manages remote relays"
@ -36,13 +35,10 @@ defmodule Mix.Tasks.Pleroma.Relay do
def run(["list"]) do
start_pleroma()
with %User{following: following} = _user <- Relay.get_actor() do
following
|> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq()
|> Enum.each(&shell_info(&1))
with {:ok, list} <- Relay.list() do
list |> Enum.each(&shell_info(&1))
else
e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end
end
end

View file

@ -161,11 +161,6 @@ defmodule Pleroma.Application do
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :federator_init,
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
restart: :temporary
}
]
end
@ -177,11 +172,6 @@ defmodule Pleroma.Application do
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :federator_init,
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
restart: :temporary
},
%{
id: :internal_fetch_init,
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},

View file

@ -48,6 +48,12 @@ defmodule Pleroma.Conversation.Participation do
|> validate_required([:read])
end
def mark_as_read(%User{} = user, %Conversation{} = conversation) do
with %__MODULE__{} = participation <- for_user_and_conversation(user, conversation) do
mark_as_read(participation)
end
end
def mark_as_read(participation) do
participation
|> read_cng(%{read: true})
@ -63,6 +69,19 @@ defmodule Pleroma.Conversation.Participation do
end
end
def mark_all_as_read(user) do
{_, participations} =
__MODULE__
|> where([p], p.user_id == ^user.id)
|> where([p], not p.read)
|> update([p], set: [read: true])
|> select([p], p)
|> Repo.update_all([])
User.set_unread_conversation_count(user)
{:ok, participations}
end
def mark_as_unread(participation) do
participation
|> read_cng(%{read: false})

View file

@ -86,18 +86,18 @@ defmodule Pleroma.ModerationLog do
parsed_datetime
end
@spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
@spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
subject: %User{} = subject,
subject: subjects,
action: action,
permission: permission
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"subject" => user_to_map(subject),
"subject" => user_to_map(subjects),
"action" => action,
"permission" => permission,
"message" => ""
@ -303,13 +303,16 @@ defmodule Pleroma.ModerationLog do
end
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
|> Repo.insert()
end
defp user_to_map(users) when is_list(users) do
users |> Enum.map(&user_to_map/1)
end
defp user_to_map(%User{} = user) do
user
|> Map.from_struct()
@ -349,10 +352,10 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "delete",
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
"subject" => subjects
}
}) do
"@#{actor_nickname} deleted user @#{subject_nickname}"
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -363,12 +366,7 @@ defmodule Pleroma.ModerationLog do
"subjects" => subjects
}
}) do
nicknames =
subjects
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
"@#{actor_nickname} created users: #{nicknames}"
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -376,10 +374,10 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
"subject" => users
}
}) do
"@#{actor_nickname} activated user @#{subject_nickname}"
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -387,10 +385,10 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
"subject" => users
}
}) do
"@#{actor_nickname} deactivated user @#{subject_nickname}"
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -402,14 +400,9 @@ defmodule Pleroma.ModerationLog do
"action" => "tag"
}
}) do
nicknames_string =
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags_string = tags |> Enum.join(", ")
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -421,14 +414,9 @@ defmodule Pleroma.ModerationLog do
"action" => "untag"
}
}) do
nicknames_string =
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags_string = tags |> Enum.join(", ")
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -436,11 +424,11 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
"subject" => %{"nickname" => subject_nickname},
"subject" => users,
"permission" => permission
}
}) do
"@#{actor_nickname} made @#{subject_nickname} #{permission}"
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -448,11 +436,11 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
"subject" => %{"nickname" => subject_nickname},
"subject" => users,
"permission" => permission
}
}) do
"@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
@ -551,4 +539,16 @@ defmodule Pleroma.ModerationLog do
}) do
"@#{actor_nickname} deleted status ##{subject_id}"
end
defp nicknames_to_string(nicknames) do
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
end
defp users_to_nicknames_string(users) do
users
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
end
end

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Notification do
import Ecto.Query
import Ecto.Changeset
require Logger
@type t :: %__MODULE__{}
@ -34,7 +35,6 @@ defmodule Pleroma.Notification do
end
def for_user_query(user, opts \\ []) do
query =
Notification
|> where(user_id: ^user.id)
|> where(
@ -54,11 +54,17 @@ defmodule Pleroma.Notification do
)
)
|> preload([n, a, o], activity: {a, object: o})
|> exclude_muted(user, opts)
|> exclude_visibility(opts)
end
if opts[:with_muted] do
defp exclude_muted(query, _, %{with_muted: true}) do
query
else
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
end
defp exclude_muted(query, user, _opts) do
query
|> where([n, a], a.actor not in ^user.info.muted_notifications)
|> where([n, a], a.actor not in ^user.info.blocks)
|> where(
[n, a],
@ -69,7 +75,51 @@ defmodule Pleroma.Notification do
)
|> where([n, a, o, tm], is_nil(tm.user_id))
end
@valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query
|> where(
[n, a],
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility in @valid_visibilities do
query
|> where(
[n, a],
not fragment(
"activity_visibility(?, ?, ?) = (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
def for_user(user, opts \\ %{}) do
user

View file

@ -181,7 +181,7 @@ defmodule Pleroma.Object do
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
safe_jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
@ -204,7 +204,7 @@ defmodule Pleroma.Object do
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
safe_jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,

View file

@ -32,6 +32,23 @@ defmodule Pleroma.Object.Containment do
get_actor(%{"actor" => actor})
end
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
# objects being present in the test suite environment. Once these objects are
# removed, please also remove this.
if Mix.env() == :test do
defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
end
defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do
if id_uri.host == other_uri.host do
:ok
else
:error
end
end
defp compare_uris(_, _), do: :error
@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
@ -41,11 +58,7 @@ defmodule Pleroma.Object.Containment do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
if id_uri.host == actor_uri.host do
:ok
else
:error
end
compare_uris(actor_uri, id_uri)
end
def contain_origin(id, %{"attributedTo" => actor} = params),
@ -57,11 +70,7 @@ defmodule Pleroma.Object.Containment do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
if id_uri.host == other_uri.host do
:ok
else
:error
end
compare_uris(id_uri, other_uri)
end
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),

View file

@ -10,7 +10,6 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus
require Logger
require Pleroma.Constants
@ -67,7 +66,8 @@ defmodule Pleroma.Object.Fetcher do
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:transmogrifier, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
@ -75,9 +75,12 @@ defmodule Pleroma.Object.Fetcher do
{:containment, _} ->
{:error, "Object containment failed."}
{:error, {:reject, nil}} ->
{:transmogrifier, {:error, {:reject, nil}}} ->
{:reject, nil}
{:transmogrifier, _} ->
{:error, "Transmogrifier failure."}
{:object, data, nil} ->
reinject_object(%Object{}, data)
@ -87,15 +90,8 @@ defmodule Pleroma.Object.Fetcher do
{:fetch_object, %Object{} = object} ->
{:ok, object}
_e ->
# Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
e ->
e
end
end
@ -114,7 +110,8 @@ defmodule Pleroma.Object.Fetcher do
with {:ok, object} <- fetch_object_from_id(id, options) do
object
else
_e ->
e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil
end
end
@ -161,7 +158,7 @@ defmodule Pleroma.Object.Fetcher do
Logger.debug("Fetch headers: #{inspect(headers)}")
with true <- String.starts_with?(id, "http"),
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
{:ok, data} <- Jason.decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
@ -170,6 +167,9 @@ defmodule Pleroma.Object.Fetcher do
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
{:scheme, _} ->
{:error, "Unsupported URI scheme"}
e ->
{:error, e}
end

View file

@ -105,7 +105,7 @@ defmodule Pleroma.Upload do
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end
opts = %{
%{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
@ -118,37 +118,6 @@ defmodule Pleroma.Upload do
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
)
}
# TODO: 1.0+ : remove old config compatibility
opts =
if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
Logger.warn("""
Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
:pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"]
""")
Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"])
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
else
opts
end
if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
Logger.warn("""
Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
""")
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
else
opts
end
end
defp prepare_upload(%Plug.Upload{} = file, opts) do

View file

@ -26,9 +26,7 @@ defmodule Pleroma.User do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.OStatus
alias Pleroma.Web.RelMe
alias Pleroma.Web.Websub
alias Pleroma.Workers.BackgroundWorker
require Logger
@ -90,6 +88,9 @@ defmodule Pleroma.User do
def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
def superuser?(_), do: false
def invisible?(%User{info: %User.Info{invisible: true}}), do: true
def invisible?(_), do: false
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
@ -437,10 +438,6 @@ defmodule Pleroma.User do
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed)
end
q =
from(u in User,
where: u.id == ^follower.id,
@ -614,12 +611,7 @@ defmodule Pleroma.User do
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
end
def fetch_by_nickname(nickname) do
case ActivityPub.make_user_from_nickname(nickname) do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
end
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
@ -725,7 +717,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
"safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
u.info,
u.info
)
@ -746,7 +738,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
"safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
u.info,
u.info
)
@ -816,7 +808,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
"safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
@ -1059,7 +1051,15 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
def deactivate(%User{} = user, status \\ true) do
def deactivate(user, status \\ true)
def deactivate(users, status) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: deactivate(user, status)
end)
end
def deactivate(%User{} = user, status) do
with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
Enum.each(get_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/1)
@ -1072,6 +1072,10 @@ defmodule Pleroma.User do
update_info(user, &User.Info.update_notification_settings(&1, settings))
end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
@ -1234,18 +1238,7 @@ defmodule Pleroma.User do
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id) do
case ActivityPub.make_user_from_ap_id(ap_id) do
{:ok, user} ->
{:ok, user}
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> {:ok, user}
_ -> {:error, "Could not fetch by AP id"}
end
end
end
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id)
@ -1300,11 +1293,6 @@ defmodule Pleroma.User do
{:ok, key}
end
# OStatus Magic Key
def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
def public_key_from_info(_), do: {:error, "not found key"}
def get_public_key_for_ap_id(ap_id) do
@ -1625,6 +1613,12 @@ defmodule Pleroma.User do
`fun` is called with the `user.info`.
"""
def update_info(users, fun) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: update_info(user, fun)
end)
end
def update_info(user, fun) do
user
|> change_info(fun)

View file

@ -39,9 +39,6 @@ defmodule Pleroma.User.Info do
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, :string, default: nil)
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
@ -56,6 +53,7 @@ defmodule Pleroma.User.Info do
field(:fields, {:array, :map}, default: nil)
field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:notification_settings, :map,
default: %{
@ -262,9 +260,6 @@ defmodule Pleroma.User.Info do
:locked,
:magic_key,
:uri,
:hub,
:topic,
:salmon,
:hide_followers,
:hide_follows,
:hide_followers_count,
@ -272,7 +267,8 @@ defmodule Pleroma.User.Info do
:follower_count,
:fields,
:following_count,
:discoverable
:discoverable,
:invisible
])
|> validate_fields(true)
end
@ -399,6 +395,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:source_data])
end
def set_invisible(info, invisible) do
params = %{invisible: invisible}
info
|> cast(params, [:invisible])
|> validate_required([:invisible])
end
def admin_api_update(info, params) do
info
|> cast(params, [

View file

@ -4,11 +4,9 @@
defmodule Pleroma.User.Search do
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
@similarity_threshold 0.25
@limit 20
def search(query_string, opts \\ []) do
@ -23,18 +21,10 @@ defmodule Pleroma.User.Search do
maybe_resolve(resolve, for_user, query_string)
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(
Repo,
"select set_limit(#{@similarity_threshold})",
[]
)
results =
query_string
|> search_query(for_user, following)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
end)
results
end
@ -56,15 +46,65 @@ defmodule Pleroma.User.Search do
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_blocked_domains(for_user)
|> search_subqueries(query_string)
|> union_subqueries
|> distinct_query()
|> boost_search_rank_query(for_user)
|> fts_search(query_string)
|> trigram_rank(query_string)
|> boost_search_rank(for_user)
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
end
@nickname_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~\-@]+$/
defp fts_search(query, query_string) do
{nickname_weight, name_weight} =
if String.match?(query_string, @nickname_regex) do
{"A", "B"}
else
{"B", "A"}
end
query_string = to_tsquery(query_string)
from(
u in query,
where:
fragment(
"""
(setweight(to_tsvector('simple', ?), ?) || setweight(to_tsvector('simple', ?), ?)) @@ to_tsquery('simple', ?)
""",
u.name,
^name_weight,
u.nickname,
^nickname_weight,
^query_string
)
)
end
defp to_tsquery(query_string) do
String.trim_trailing(query_string, "@" <> local_domain())
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
end
defp trigram_rank(query, query_string) do
from(
u in query,
select_merge: %{
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^query_string,
u.nickname,
u.name
)
}
)
end
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
@ -87,21 +127,6 @@ defmodule Pleroma.User.Search do
defp filter_blocked_domains(query, _), do: query
defp union_subqueries({fts_subquery, trigram_subquery}) do
from(s in trigram_subquery, union_all: ^fts_subquery)
end
defp search_subqueries(base_query, query_string) do
{
fts_search_subquery(base_query, query_string),
trigram_search_subquery(base_query, query_string)
}
end
defp distinct_query(q) do
from(s in subquery(q), order_by: s.search_type, distinct: s.id)
end
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
@ -126,9 +151,9 @@ defmodule Pleroma.User.Search do
defp restrict_local(q), do: where(q, [u], u.local == true)
defp boost_search_rank_query(query, nil), do: query
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
defp boost_search_rank_query(query, for_user) do
defp boost_search_rank(query, %User{} = for_user) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
@ -137,8 +162,8 @@ defmodule Pleroma.User.Search do
search_rank:
fragment(
"""
CASE WHEN (?) THEN 0.5 + (?) * 1.3
WHEN (?) THEN 0.5 + (?) * 1.2
CASE WHEN (?) THEN (?) * 1.5
WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
@ -154,70 +179,5 @@ defmodule Pleroma.User.Search do
)
end
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp fts_search_subquery(query, term) do
processed_query =
String.trim_trailing(term, "@" <> local_domain())
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
ts_rank_cd(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
to_tsquery('simple', ?),
32
)
""",
u.nickname,
u.name,
^processed_query
)
},
where:
fragment(
"""
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^processed_query
)
)
|> User.restrict_deactivated()
end
@spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp trigram_search_subquery(query, term) do
term = String.trim_trailing(term, "@" <> local_domain())
from(
u in query,
select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
|> User.restrict_deactivated()
end
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
defp boost_search_rank(query, _for_user), do: query
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
@ -131,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
:ok <- Containment.contain_child(map),
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
@ -153,11 +154,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Notification.create_notifications(activity)
participations =
activity
|> Conversation.create_or_bump_for()
|> get_participations()
conversation = create_or_bump_conversation(activity, map["actor"])
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
@ -182,7 +180,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
defp get_participations({:ok, %{participations: participations}}), do: participations
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor),
Participation.mark_as_read(user, conversation) do
{:ok, conversation}
end
end
defp get_participations({:ok, conversation}) do
conversation
|> Repo.preload(:participations, force: true)
|> Map.get(:participations)
end
defp get_participations(_), do: []
def stream_out_participations(participations) do
@ -225,6 +236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
quick_insert? = Pleroma.Config.get([:env]) == :benchmark
with create_data <-
make_create_data(
@ -235,12 +247,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:quick_insert, true, activity} ->
{:ok, activity}
{:fake, true, activity} ->
{:ok, activity}
@ -596,6 +612,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, _visibility), do: query
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ?",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
@ -960,6 +1019,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
|> exclude_visibility(opts)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
@ -1046,6 +1106,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
user_data = %{
ap_id: data["id"],
@ -1055,7 +1116,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
banner: banner,
fields: fields,
locked: locked,
discoverable: discoverable
discoverable: discoverable,
invisible: invisible
},
avatar: avatar,
name: data["name"],
@ -1159,7 +1221,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
data <- maybe_update_follow_information(data) do
{:ok, data}
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
e ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
end
end

View file

@ -129,7 +129,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
[]
end
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers ++ fetchers
Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
end
defp get_cc_ap_ids(ap_id, recipients) do

View file

@ -10,8 +10,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
require Logger
def get_actor do
actor =
"#{Pleroma.Web.Endpoint.url()}/relay"
|> User.get_or_create_service_actor_by_ap_id()
{:ok, actor} = User.update_info(actor, &User.Info.set_invisible(&1, true))
actor
end
@spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
@ -51,6 +55,20 @@ defmodule Pleroma.Web.ActivityPub.Relay do
def publish(_), do: {:error, "Not implemented"}
@spec list() :: {:ok, [String.t()]} | {:error, any()}
def list do
with %User{following: following} = _user <- get_actor() do
list =
following
|> Enum.map(fn entry -> URI.parse(entry).host end)
|> Enum.uniq()
{:ok, list}
else
error -> format_error(error)
end
end
defp format_error({:error, error}), do: format_error(error)
defp format_error(error) do

View file

@ -596,13 +596,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data,
_options
)
when object_type in ["Person", "Application", "Service", "Organization"] do
when object_type in [
"Person",
"Application",
"Service",
"Organization"
] do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info][:banner]
locked = new_user_data[:info][:locked] || false
attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
invisible = new_user_data[:info][:invisible] || false
fields =
attachment
@ -612,7 +618,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
|> Map.put(:info, %{banner: banner, locked: locked, fields: fields, invisible: invisible})
actor
|> User.upgrade_changeset(update_data, true)
@ -1073,8 +1079,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Repo.update_all(q, [])
maybe_retire_websub(user.ap_id)
q =
from(
a in Activity,
@ -1117,19 +1121,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> User.update_and_set_cache()
end
def maybe_retire_websub(ap_id) do
# some sanity checks
if is_binary(ap_id) && String.length(ap_id) > 8 do
q =
from(
ws in Pleroma.Web.Websub.WebsubClientSubscription,
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
)
Repo.delete_all(q)
end
end
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end

View file

@ -491,11 +491,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
%Activity{data: %{"actor" => actor}},
object
) do
unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
announcements = take_announcements(object)
with announcements <- Enum.uniq([actor | announcements]) do
update_element_in_object("announcement", announcements, object)
end
else
{:ok, object}
end
end
def add_announce_to_object(_, object), do: {:ok, object}

View file

@ -55,7 +55,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints
"endpoints" => endpoints,
"invisible" => User.invisible?(user)
}
|> Map.merge(Utils.make_json_ld_header())
end

View file

@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:user_delete,
:users_create,
:user_toggle_activation,
:user_activate,
:user_deactivate,
:tag_users,
:untag_users,
:right_add,
@ -98,7 +100,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
actor: admin,
subject: user,
subject: [user],
action: "delete"
})
@ -106,6 +108,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(nickname)
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.delete(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "delete"
})
conn
|> json(nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
@ -240,7 +256,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
actor: admin,
subject: user,
subject: [user],
action: action
})
@ -249,6 +265,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("show.json", %{user: updated_user})
end
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, false)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "activate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, true)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "deactivate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
@ -313,6 +359,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> Enum.into(%{}, &{&1, true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nicknames" => nicknames
})
when permission_group in ["moderator", "admin"] do
info = Map.put(%{}, "is_" <> permission_group, true)
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.update_info(users, &User.Info.admin_api_update(&1, info))
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, info)
end
def right_add_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_add(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nickname" => nickname
@ -328,7 +399,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: user,
subject: [user],
permission: permission_group
})
@ -349,8 +420,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
def right_delete_multiple(
%{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
%{
"permission_group" => permission_group,
"nicknames" => nicknames
}
)
when permission_group in ["moderator", "admin"] do
with false <- Enum.member?(nicknames, admin_nickname) do
info = Map.put(%{}, "is_" <> permission_group, false)
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.update_info(users, &User.Info.admin_api_update(&1, info))
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: users,
permission: permission_group
})
json(conn, info)
else
_ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
end
end
def right_delete_multiple(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_delete(
@ -371,33 +470,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: user,
subject: [user],
permission: permission_group
})
json(conn, info)
end
def right_delete(conn, _) do
render_error(conn, :not_found, "No such permission_group")
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
def set_activation_status(%{assigns: %{user: admin}} = conn, %{
"nickname" => nickname,
"status" => status
}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status) do
action = if(user.info.deactivated, do: "activate", else: "deactivate")
ModerationLog.insert_log(%{
actor: admin,
subject: user,
action: action
})
json_response(conn, :no_content, "")
def relay_list(conn, _params) do
with {:ok, list} <- Relay.list() do
json(conn, %{relays: list})
else
_ ->
conn
|> put_status(500)
end
end

View file

@ -19,6 +19,12 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
def render("index.json", %{users: users}) do
%{
users: render_many(users, AccountView, "show.json", as: :user)
}
end
def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = HTML.strip_tags(user.name || user.nickname)

View file

@ -10,19 +10,11 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
alias Pleroma.Workers.SubscriberWorker
require Logger
def init do
# To do: consider removing this call in favor of scheduled execution (`quantum`-based)
refresh_subscriptions(schedule_in: 60)
end
@doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
def allowed_incoming_reply_depth?(depth) do
@ -37,10 +29,6 @@ defmodule Pleroma.Web.Federator do
# Client API
def incoming_doc(doc) do
ReceiverWorker.enqueue("incoming_doc", %{"body" => doc})
end
def incoming_ap_doc(params) do
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
end
@ -53,18 +41,6 @@ defmodule Pleroma.Web.Federator do
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
end
def verify_websub(websub) do
SubscriberWorker.enqueue("verify_websub", %{"websub_id" => websub.id})
end
def request_subscription(websub) do
SubscriberWorker.enqueue("request_subscription", %{"websub_id" => websub.id})
end
def refresh_subscriptions(worker_args \\ []) do
SubscriberWorker.enqueue("refresh_subscriptions", %{}, worker_args ++ [max_attempts: 1])
end
# Job Worker Callbacks
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
@ -81,11 +57,6 @@ defmodule Pleroma.Web.Federator do
end
end
def perform(:incoming_doc, doc) do
Logger.info("Got document, trying to parse")
OStatus.handle_incoming(doc)
end
def perform(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity")
@ -111,29 +82,6 @@ defmodule Pleroma.Web.Federator do
end
end
def perform(:request_subscription, websub) do
Logger.debug("Refreshing #{websub.topic}")
with {:ok, websub} <- Websub.request_subscription(websub) do
Logger.debug("Successfully refreshed #{websub.topic}")
else
_e -> Logger.debug("Couldn't refresh #{websub.topic}")
end
end
def perform(:verify_websub, websub) do
Logger.debug(fn ->
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
end)
Websub.verify(websub)
end
def perform(:refresh_subscriptions) do
Logger.debug("Federator running refresh subscriptions")
Websub.refresh_subscriptions()
end
def ap_enabled_actor(id) do
user = User.get_cached_by_ap_id(id)

View file

@ -80,4 +80,30 @@ defmodule Pleroma.Web.Federator.Publisher do
links ++ module.gather_nodeinfo_protocol_names()
end)
end
@doc """
Gathers a set of remote users given an IR envelope.
"""
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
cc = Map.get(data, "cc", [])
bcc =
data
|> Map.get("bcc", [])
|> Enum.reduce([], fn ap_id, bcc ->
case Pleroma.List.get_by_ap_id(ap_id) do
%Pleroma.List{user_id: ^user_id} = list ->
{:ok, following} = Pleroma.List.get_following(list)
bcc ++ Enum.map(following, & &1.ap_id)
_ ->
bcc
end
end)
[to, cc, bcc]
|> Enum.concat()
|> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user && !user.local end)
end
end

View file

@ -34,6 +34,12 @@ defmodule Pleroma.Web.MastoFEController do
end
end
@doc "GET /web/manifest.json"
def manifest(conn, _params) do
conn
|> render("manifest.json")
end
@doc "PUT /api/web/settings"
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do

View file

@ -71,6 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean
}

View file

@ -35,6 +35,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
topic when is_binary(topic) <- expand_topic(stream, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
else
req
end
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
else
{:error, code} ->

View file

@ -1,313 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.OStatus.UserRepresenter
require Logger
require Pleroma.Constants
defp get_href(id) do
with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do
external_url
else
_e -> id
end
end
defp get_in_reply_to(activity) do
with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do
[
{:"thr:in-reply-to",
[ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
]
else
_ ->
[]
end
end
defp get_mentions(to) do
Enum.map(to, fn id ->
cond do
# Special handling for the AP/Ostatus public collections
Pleroma.Constants.as_public() == id ->
{:link,
[
rel: "mentioned",
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection",
href: "http://activityschema.org/collection/public"
], []}
# Ostatus doesn't handle follower collections, ignore these.
Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) ->
[]
true ->
{:link,
[
rel: "mentioned",
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/person",
href: id
], []}
end
end)
end
defp get_links(%{local: true}, %{"id" => object_id}) do
h = fn str -> [to_charlist(str)] end
[
{:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
]
end
defp get_links(%{local: false}, %{"external_url" => external_url}) do
h = fn str -> [to_charlist(str)] end
[
{:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []}
]
end
defp get_links(_activity, _object_data), do: []
defp get_emoji_links(emojis) do
Enum.map(emojis, fn {emoji, file} ->
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
end)
end
def to_simple_form(activity, user, with_author \\ false)
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
object = Object.normalize(activity)
updated_at = object.data["published"]
inserted_at = object.data["published"]
attachments =
Enum.map(object.data["attachment"] || [], fn attachment ->
url = hd(attachment["url"])
{:link,
[rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])],
[]}
end)
in_reply_to = get_in_reply_to(activity)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions
categories =
(object.data["tag"] || [])
|> Enum.map(fn tag ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
else
nil
end
end)
|> Enum.filter(& &1)
emoji_links = get_emoji_links(object.data["emoji"] || %{})
summary =
if object.data["summary"] do
[{:summary, [], h.(object.data["summary"])}]
else
[]
end
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
# For notes, federate the object id.
{:id, h.(object.data["id"])},
{:title, ['New note by #{user.nickname}']},
{:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++
summary ++
get_links(activity, object.data) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions
[
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
{:id, h.(activity.data["id"])},
{:title, ['New favorite by #{user.nickname}']},
{:content, [type: 'html'], ['#{user.nickname} favorited something']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"activity:object",
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
# For notes, federate the object id.
{:id, h.(activity.data["object"])}
]},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
{:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []}
] ++ author ++ mentions
end
def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
retweeted_object = Object.normalize(retweeted_activity)
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
mentions =
([retweeted_user.ap_id] ++ activity.recipients)
|> Enum.uniq()
|> get_mentions()
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
{:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} repeated a notice']},
{:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
{:"activity:object", retweeted_xml}
] ++ mentions ++ author
end
def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.recipients || []) |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
{:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} started following #{activity.data["object"]}']},
{:content, [type: 'html'],
['#{user.nickname} started following #{activity.data["object"]}']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"activity:object",
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(activity.data["object"])},
{:uri, h.(activity.data["object"])}
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author
end
# Only undos of follow for now. Will need to get redone once there are more
def to_simple_form(
%{data: %{"type" => "Undo", "object" => %{"type" => "Follow"} = follow_activity}} =
activity,
user,
with_author
) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.recipients || []) |> get_mentions
follow_activity = Activity.normalize(follow_activity)
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
{:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
{:content, [type: 'html'],
['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"activity:object",
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(follow_activity.data["object"])},
{:uri, h.(follow_activity.data["object"])}
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author
end
def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/delete']},
{:id, h.(activity.data["object"])},
{:title, ['An object was deleted']},
{:content, [type: 'html'], ['An object was deleted']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)}
] ++ author
end
def to_simple_form(_, _, _), do: nil
def wrap_with_entry(simple_form) do
[
{
:entry,
[
xmlns: 'http://www.w3.org/2005/Atom',
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
],
simple_form
}
]
end
end

View file

@ -1,66 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.UserRepresenter
def to_simple_form(user, activities, _users) do
most_recent_update =
(List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601()
h = fn str -> [to_charlist(str)] end
last_activity = List.last(activities)
entries =
activities
|> Enum.map(fn activity ->
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
end)
|> Enum.filter(fn {_, form} -> form end)
[
{
:feed,
[
xmlns: 'http://www.w3.org/2005/Atom',
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
],
[
{:id, h.(OStatus.feed_path(user))},
{:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)},
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'],
[]},
{:author, UserRepresenter.to_simple_form(user)}
] ++
if last_activity do
[
{:link,
[
rel: 'next',
href:
to_charlist(OStatus.feed_path(user)) ++
'?max_id=' ++ to_charlist(last_activity.id),
type: 'application/atom+xml'
], []}
]
else
[]
end ++ entries
}
]
end
end

View file

@ -1,18 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.DeleteHandler do
require Logger
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.XML
def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
%Object{} = object <- Object.normalize(id),
{:ok, delete} <- ActivityPub.delete(object, local: false) do
delete
end
end
end

View file

@ -1,26 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FollowHandler do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
{:locked, false} <- {:locked, followed.info.locked},
{:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do
User.follow(actor, followed)
{:ok, activity}
else
{:locked, true} ->
{:error, "It's not possible to follow locked accounts over OStatus"}
end
end
end

View file

@ -1,168 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.NoteHandler do
require Logger
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
@doc """
Get the context for this note. Uses this:
1. The context of the parent activity
2. The conversation reference in the ostatus xml
3. A newly generated context id.
"""
def get_context(entry, in_reply_to) do
context =
(XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|> String.trim()
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do
context
else
_e ->
if String.length(context) > 0 do
context
else
Utils.generate_context_id()
end
end
end
def get_people_mentions(entry) do
:xmerl_xpath.string(
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]',
entry
)
|> Enum.map(fn person -> XML.string_from_xpath("@href", person) end)
end
def get_collection_mentions(entry) do
transmogrify = fn
"http://activityschema.org/collection/public" ->
Pleroma.Constants.as_public()
group ->
group
end
:xmerl_xpath.string(
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]',
entry
)
|> Enum.map(fn collection -> XML.string_from_xpath("@href", collection) |> transmogrify.() end)
end
def get_mentions(entry) do
(get_people_mentions(entry) ++ get_collection_mentions(entry))
|> Enum.filter(& &1)
end
def get_emoji(entry) do
try do
:xmerl_xpath.string('//link[@rel="emoji"]', entry)
|> Enum.reduce(%{}, fn emoji, acc ->
Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji))
end)
rescue
_e -> nil
end
end
def make_to_list(actor, mentions) do
[
actor.follower_address
] ++ mentions
end
def add_external_url(note, entry) do
url = XML.string_from_xpath("//link[@rel='alternate' and @type='text/html']/@href", entry)
Map.put(note, "external_url", url)
end
def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
activity
else
_e ->
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
in_reply_to_href when not is_nil(in_reply_to_href) <-
XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do
activity
else
_e -> nil
end
end
end
# TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil, options \\ []) do
with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
[author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_actor(author),
content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry),
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1),
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options),
in_reply_to_object <-
(in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
attachments <- OStatus.get_attachments(entry),
context <- get_context(entry, in_reply_to),
tags <- OStatus.get_tags(entry),
mentions <- get_mentions(entry),
to <- make_to_list(actor, mentions),
date <- XML.string_from_xpath("//published", entry),
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []),
note <-
CommonAPI.Utils.make_note_data(
actor.ap_id,
to,
context,
content_html,
attachments,
in_reply_to_activity,
[],
cw
),
note <- note |> Map.put("id", id) |> Map.put("tag", tags),
note <- note |> Map.put("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)),
note <- add_external_url(note, entry),
note <- note |> Map.put("cc", cc),
# TODO: Handle this case in make_note_data
note <-
if(
in_reply_to && !in_reply_to_activity,
do: note |> Map.put("inReplyTo", in_reply_to),
else: note
) do
ActivityPub.create(%{
to: to,
actor: actor,
context: context,
object: note,
published: date,
local: false,
additional: %{"cc" => cc}
})
else
%Activity{} = activity -> {:ok, activity}
e -> {:error, e}
end
end
end

View file

@ -1,22 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.UnfollowHandler do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
{:ok, activity} <- ActivityPub.unfollow(actor, followed, id, false) do
User.unfollow(actor, followed)
{:ok, activity}
end
end
end

View file

@ -1,395 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus do
import Pleroma.Web.XML
require Logger
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler
alias Pleroma.Web.OStatus.UnfollowHandler
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
def is_representable?(%Activity{} = activity) do
object = Object.normalize(activity)
cond do
is_nil(object) ->
false
Visibility.is_public?(activity) && object.data["type"] == "Note" ->
true
true ->
false
end
end
def feed_path(user), do: "#{user.ap_id}/feed.atom"
def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
def salmon_path(user), do: "#{user.ap_id}/salmon"
def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
def handle_incoming(xml_string, options \\ []) do
with doc when doc != :error <- parse_document(xml_string) do
with {:ok, actor_user} <- find_make_or_update_actor(doc),
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
entries = :xmerl_xpath.string('//entry', doc)
activities =
Enum.map(entries, fn entry ->
{:xmlObj, :string, object_type} =
:xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
{:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
Logger.debug("Handling #{verb}")
try do
case verb do
'http://activitystrea.ms/schema/1.0/delete' ->
with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/follow' ->
with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/unfollow' ->
with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/share' ->
with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
do: [activity, retweeted_activity]
'http://activitystrea.ms/schema/1.0/favorite' ->
with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
do: [activity, favorited_activity]
_ ->
case object_type do
'http://activitystrea.ms/schema/1.0/note' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
do: activity
'http://activitystrea.ms/schema/1.0/comment' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
do: activity
_ ->
Logger.error("Couldn't parse incoming document")
nil
end
end
rescue
e ->
Logger.error("Error occured while handling activity")
Logger.error(xml_string)
Logger.error(inspect(e))
nil
end
end)
|> Enum.filter(& &1)
{:ok, activities}
else
_e -> {:error, []}
end
end
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_actor(doc),
%Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
end
end
def handle_share(entry, doc) do
with {:ok, retweeted_activity} <- get_or_build_object(entry),
{:ok, activity} <- make_share(entry, doc, retweeted_activity) do
{:ok, activity, retweeted_activity}
else
e -> {:error, e}
end
end
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_actor(doc),
%Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}
end
end
def get_or_build_object(entry) do
with {:ok, activity} <- get_or_try_fetching(entry) do
{:ok, activity}
else
_e ->
with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
NoteHandler.handle_note(object, object)
end
end
end
def get_or_try_fetching(entry) do
Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
{:ok, activity}
else
_ ->
Logger.debug("Couldn't get, will try to fetch")
with href when not is_nil(href) <-
string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
{:ok, favorited_activity}
else
e -> Logger.debug("Couldn't find href: #{inspect(e)}")
end
end
end
def handle_favorite(entry, doc) do
with {:ok, favorited_activity} <- get_or_try_fetching(entry),
{:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
{:ok, activity, favorited_activity}
else
e -> {:error, e}
end
end
def get_attachments(entry) do
:xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
|> Enum.map(fn enclosure ->
with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
%{
"type" => "Attachment",
"url" => [
%{
"type" => "Link",
"mediaType" => type,
"href" => href
}
]
}
end
end)
|> Enum.filter(& &1)
end
@doc """
Gets the content from a an entry.
"""
def get_content(entry) do
string_from_xpath("//content", entry)
end
@doc """
Get the cw that mastodon uses.
"""
def get_cw(entry) do
case string_from_xpath("/*/summary", entry) do
cw when not is_nil(cw) -> cw
_ -> nil
end
end
def get_tags(entry) do
:xmerl_xpath.string('//category', entry)
|> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
|> Enum.filter(& &1)
|> Enum.map(&String.downcase/1)
end
def maybe_update(doc, user) do
case string_from_xpath("//author[1]/ap_enabled", doc) do
"true" ->
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
_ ->
maybe_update_ostatus(doc, user)
end
end
def maybe_update_ostatus(doc, user) do
old_data = Map.take(user, [:bio, :avatar, :name])
with false <- user.local,
avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc),
new_data <- %{
avatar: avatar || old_data.avatar,
name: name || old_data.name,
bio: bio || old_data.bio
},
false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data)
User.update_and_set_cache(change)
else
_ ->
{:ok, user}
end
end
def find_make_or_update_actor(doc) do
uri = string_from_xpath("//author/uri[1]", doc)
with {:ok, %User{} = user} <- find_or_make_user(uri),
{:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do
maybe_update(doc, user)
else
{:ap_enabled, true} ->
{:error, :invalid_protocol}
_ ->
{:error, :unknown_user}
end
end
@spec find_or_make_user(String.t()) :: {:ok, User.t()}
def find_or_make_user(uri) do
case User.get_by_ap_id(uri) do
%User{} = user -> {:ok, user}
_ -> make_user(uri)
end
end
@spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()}
def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do
with false <- update,
%User{} = user <- User.get_cached_by_ap_id(info["uri"]) do
{:ok, user}
else
_e -> User.insert_or_update_user(build_user_data(info))
end
end
end
defp build_user_data(info) do
%{
name: info["name"],
nickname: info["nickname"] <> "@" <> info["host"],
ap_id: info["uri"],
info: info,
avatar: info["avatar"],
bio: info["bio"]
}
end
# TODO: Just takes the first one for now.
def make_avatar_object(author_doc, rel \\ "avatar") do
href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
if href do
%{
"type" => "Image",
"url" => [%{"type" => "Link", "mediaType" => type, "href" => href}]
}
else
nil
end
end
@spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()}
def gather_user_info(username) do
with {:ok, webfinger_data} <- WebFinger.finger(username),
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
data =
webfinger_data
|> Map.merge(feed_data)
|> Map.put("fqn", username)
{:ok, data}
else
e ->
Logger.debug(fn -> "Couldn't gather info for #{username}" end)
{:error, e}
end
end
# Regex-based 'parsing' so we don't have to pull in a full html parser
# It's a hack anyway. Maybe revisit this in the future
@mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
@gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
@gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
def get_atom_url(body) do
cond do
Regex.match?(@mastodon_regex, body) ->
[[_, match]] = Regex.scan(@mastodon_regex, body)
{:ok, match}
Regex.match?(@gs_regex, body) ->
[[_, match]] = Regex.scan(@gs_regex, body)
{:ok, match}
Regex.match?(@gs_classic_regex, body) ->
[[_, match]] = Regex.scan(@gs_classic_regex, body)
{:ok, match}
true ->
Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
{:error, "Couldn't find the Atom link"}
end
end
def fetch_activity_from_atom_url(url, options \\ []) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
HTTP.get(url, [{:Accept, "application/atom+xml"}]) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body, options)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
def fetch_activity_from_html_url(url, options \\ []) do
Logger.debug("Trying to fetch #{url}")
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body}} <- HTTP.get(url, []),
{:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url, options)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
def fetch_activity_from_url(url, options \\ []) do
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
{:ok, activities}
else
_e -> fetch_activity_from_html_url(url, options)
end
rescue
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
{:error, "Couldn't get #{url}: #{inspect(e)}"}
end
end

View file

@ -13,19 +13,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.Metadata.PlayerView
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.Router
alias Pleroma.Web.XML
plug(
Pleroma.Plugs.RateLimiter,
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
)
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
plug(
Pleroma.Plugs.SetFormatPlug
when action in [:object, :activity, :notice]
@ -33,32 +28,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
action_fallback(:errors)
defp decode_or_retry(body) do
with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
{:ok, doc}
else
_e ->
with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
doc <- XML.parse_document(decoded),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
{:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
{:ok, doc}
end
end
end
def salmon_incoming(conn, _) do
{:ok, body, _conn} = read_body(conn)
{:ok, doc} = decode_or_retry(body)
Federator.incoming_doc(doc)
send_resp(conn, 200, "")
end
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
@ -179,23 +148,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|> render("object.json", %{object: object})
end
defp represent_activity(_conn, "activity+json", _, _) do
defp represent_activity(_conn, _, _, _) do
{:error, :not_found}
end
defp represent_activity(conn, _, activity, user) do
response =
activity
|> ActivityRepresenter.to_simple_form(user, true)
|> ActivityRepresenter.wrap_with_entry()
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end

View file

@ -1,41 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.UserRepresenter do
alias Pleroma.User
def to_simple_form(user) do
ap_id = to_charlist(user.ap_id)
nickname = to_charlist(user.nickname)
name = to_charlist(user.name)
bio = to_charlist(user.bio)
avatar_url = to_charlist(User.avatar_url(user))
banner =
if banner_url = User.banner_url(user) do
[{:link, [rel: 'header', href: banner_url], []}]
else
[]
end
ap_enabled =
if user.local do
[{:ap_enabled, ['true']}]
else
[]
end
[
{:id, [ap_id]},
{:"activity:object", ['http://activitystrea.ms/schema/1.0/person']},
{:uri, [ap_id]},
{:"poco:preferredUsername", [nickname]},
{:"poco:displayName", [name]},
{:"poco:note", [bio]},
{:summary, [bio]},
{:name, [nickname]},
{:link, [rel: 'avatar', href: avatar_url], []}
] ++ banner ++ ap_enabled
end
end

View file

@ -79,6 +79,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
def read_conversations(%{assigns: %{user: user}} = conn, _params) do
with {:ok, participations} <- Participation.mark_all_as_read(user) do
conn
|> add_link_headers(participations)
|> put_view(ConversationView)
|> render("participations.json", participations: participations, for: user)
end
end
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do
conn

View file

@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
patch("/users/activate", AdminAPIController, :user_activate)
patch("/users/deactivate", AdminAPIController, :user_deactivate)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
@ -150,8 +153,15 @@ defmodule Pleroma.Web.Router do
:right_delete
)
put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
get("/relay", AdminAPIController, :relay_list)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
@ -256,6 +266,7 @@ defmodule Pleroma.Web.Router do
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :read_conversations)
end
scope [] do
@ -502,11 +513,6 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/feed", Feed.FeedController, :feed)
get("/users/:nickname", Feed.FeedController, :feed_redirect)
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
@ -589,6 +595,12 @@ defmodule Pleroma.Web.Router do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/web/manifest.json", MastoFEController, :manifest)
end
scope "/", Pleroma.Web do
pipe_through(:mastodon_html)

View file

@ -1,254 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon do
@behaviour Pleroma.Web.Federator.Publisher
use Bitwise
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.XML
require Logger
def decode(salmon) do
doc = XML.parse_document(salmon)
{:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
{:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
{:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
{:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
{:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
{:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
{:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
alg = to_string(alg)
encoding = to_string(encoding)
type = to_string(type)
[data, type, encoding, alg, sig]
end
def fetch_magic_key(salmon) do
with [data, _, _, _, _] <- decode(salmon),
doc <- XML.parse_document(data),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, public_key} <- User.get_public_key_for_ap_id(uri),
magic_key <- encode_key(public_key) do
{:ok, magic_key}
end
end
def decode_and_validate(magickey, salmon) do
[data, type, encoding, alg, sig] = decode(salmon)
signed_text =
[data, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
key = decode_key(magickey)
verify = :public_key.verify(signed_text, :sha256, sig, key)
if verify do
{:ok, data}
else
:error
end
end
def decode_key("RSA." <> magickey) do
make_integer = fn bin ->
list = :erlang.binary_to_list(bin)
Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
end
[modulus, exponent] =
magickey
|> String.split(".")
|> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
|> Enum.map(make_integer)
{:RSAPublicKey, modulus, exponent}
end
def encode_key({:RSAPublicKey, modulus, exponent}) do
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
"RSA.#{modulus_enc}.#{exponent_enc}"
end
def encode(private_key, doc) do
type = "application/atom+xml"
encoding = "base64url"
alg = "RSA-SHA256"
signed_text =
[doc, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signature =
signed_text
|> :public_key.sign(:sha256, private_key)
|> to_string
|> Base.url_encode64()
doc_base64 =
doc
|> Base.url_encode64()
# Don't need proper xml building, these strings are safe to leave unescaped
salmon = """
<?xml version="1.0" encoding="UTF-8"?>
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
<me:data type="application/atom+xml">#{doc_base64}</me:data>
<me:encoding>#{encoding}</me:encoding>
<me:alg>#{alg}</me:alg>
<me:sig>#{signature}</me:sig>
</me:env>
"""
{:ok, salmon}
end
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
cc = Map.get(data, "cc", [])
bcc =
data
|> Map.get("bcc", [])
|> Enum.reduce([], fn ap_id, bcc ->
case Pleroma.List.get_by_ap_id(ap_id) do
%Pleroma.List{user_id: ^user_id} = list ->
{:ok, following} = Pleroma.List.get_following(list)
bcc ++ Enum.map(following, & &1.ap_id)
_ ->
bcc
end
end)
[to, cc, bcc]
|> Enum.concat()
|> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user && !user.local end)
end
@doc "Pushes an activity to remote account."
def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
do: publish_one(Map.put(params, :recipient, salmon))
def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
with {:ok, %{status: code}} when code in 200..299 <-
HTTP.post(
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
{:ok, code}
else
e ->
unless params[:unreachable_since], do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
{:error, "Unreachable instance"}
end
end
def publish_one(%{recipient_id: recipient_id} = params) do
recipient = User.get_cached_by_id(recipient_id)
params
|> Map.delete(:recipient_id)
|> Map.put(:recipient, recipient)
|> publish_one()
end
def publish_one(_), do: :noop
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
@doc """
Publishes an activity to remote accounts
"""
@spec publish(User.t(), Pleroma.Activity.t()) :: none
def publish(user, activity)
def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true)
if feed do
feed =
ActivityRepresenter.wrap_with_entry(feed)
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
{:ok, private, _} = Keys.keys_from_pem(keys)
{:ok, feed} = encode(private, feed)
remote_users = remote_users(user, activity)
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
reachable_urls = Map.keys(reachable_urls_metadata)
remote_users
|> Enum.filter(&(&1.info.salmon in reachable_urls))
|> Enum.each(fn remote_user ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
Publisher.enqueue_one(__MODULE__, %{
recipient_id: remote_user.id,
feed: feed,
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
})
end)
end
end
def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
def gather_webfinger_links(%User{} = user) do
{:ok, _private, public} = Keys.keys_from_pem(user.keys)
magic_key = encode_key(public)
[
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{
"rel" => "magic-public-key",
"href" => "data:application/magic-public-key,#{magic_key}"
}
]
end
def gather_nodeinfo_protocol_names, do: []
end

View file

@ -49,7 +49,7 @@ defmodule Pleroma.Web.Streamer do
end
end
defp handle_should_send(_) do
true
end
defp handle_should_send(:benchmark), do: false
defp handle_should_send(_), do: true
end

View file

@ -10,8 +10,6 @@
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<logo><%= logo(@user) %></logo>
<link rel="hub" href="<%= websub_url(@conn, :websub_subscription_request, @user.nickname) %>"/>
<link rel="salmon" href="<%= o_status_url(@conn, :salmon_incoming, @user.nickname) %>"/>
<link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
<%= render @view_module, "_author.xml", assigns %>

View file

@ -4,9 +4,13 @@
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>
<%= Pleroma.Config.get([:instance, :name]) %>
<%= Config.get([:instance, :name]) %>
</title>
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="manifest" type="applicaton/manifest+json" href="<%= masto_fe_path(Pleroma.Web.Endpoint, :manifest) %>" />
<meta name="theme-color" content="<%= Config.get([:manifest, :theme_color]) %>" />
<script crossorigin='anonymous' src="/packs/locales.js"></script>
<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>

View file

@ -99,4 +99,23 @@ defmodule Pleroma.Web.MastoFEView do
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
def render("manifest.json", _params) do
%{
name: Config.get([:instance, :name]),
description: Config.get([:instance, :description]),
icons: Config.get([:manifest, :icons]),
theme_color: Config.get([:manifest, :theme_color]),
background_color: Config.get([:manifest, :background_color]),
display: "standalone",
scope: Pleroma.Web.base_url(),
start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]),
categories: [
"social"
],
serviceworker: %{
src: "/sw.js"
}
}
end
end

View file

@ -108,7 +108,6 @@ defmodule Pleroma.Web.WebFinger do
doc
),
subject <- XML.string_from_xpath("//Subject", doc),
salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
subscribe_address <-
XML.string_from_xpath(
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
@ -123,7 +122,6 @@ defmodule Pleroma.Web.WebFinger do
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
@ -148,16 +146,6 @@ defmodule Pleroma.Web.WebFinger do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
{_, "magic-public-key"} ->
"data:application/magic-public-key," <> magic_key = link["href"]
Map.put(data, "magic_key", magic_key)
{"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
Map.put(data, "topic", link["href"])
{_, "salmon"} ->
Map.put(data, "salmon", link["href"])
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
Map.put(data, "subscribe_address", link["template"])

View file

@ -1,332 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.Websub.WebsubServerSubscription
alias Pleroma.Web.XML
require Logger
import Ecto.Query
@behaviour Pleroma.Web.Federator.Publisher
def verify(subscription, getter \\ &HTTP.get/3) do
challenge = Base.encode16(:crypto.strong_rand_bytes(8))
lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
lease_seconds = lease_seconds |> to_string
params = %{
"hub.challenge": challenge,
"hub.lease_seconds": lease_seconds,
"hub.topic": subscription.topic,
"hub.mode": "subscribe"
}
url = hd(String.split(subscription.callback, "?"))
query = URI.parse(subscription.callback).query || ""
params = Map.merge(params, URI.decode_query(query))
with {:ok, response} <- getter.(url, [], params: params),
^challenge <- response.body do
changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset)
else
e ->
Logger.debug("Couldn't verify subscription")
Logger.debug(inspect(e))
{:error, subscription}
end
end
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
response =
user
|> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
query =
from(
sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until)
)
subscriptions = Repo.all(query)
callbacks = Enum.map(subscriptions, & &1.callback)
reachable_callbacks_metadata = Instances.filter_reachable(callbacks)
reachable_callbacks = Map.keys(reachable_callbacks_metadata)
subscriptions
|> Enum.filter(&(&1.callback in reachable_callbacks))
|> Enum.each(fn sub ->
data = %{
xml: response,
topic: topic,
callback: sub.callback,
secret: sub.secret,
unreachable_since: reachable_callbacks_metadata[sub.callback]
}
Publisher.enqueue_one(__MODULE__, data)
end)
end
def publish(_, _, _), do: ""
def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end
def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
with {:ok, topic} <- valid_topic(params, user),
{:ok, lease_time} <- lease_time(params),
secret <- params["hub.secret"],
callback <- params["hub.callback"] do
subscription = get_subscription(topic, callback)
data = %{
state: subscription.state || "requested",
topic: topic,
secret: secret,
callback: callback
}
change = Changeset.change(subscription, data)
websub = Repo.insert_or_update!(change)
change =
Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
websub = Repo.update!(change)
Federator.verify_websub(websub)
{:ok, websub}
else
{:error, reason} ->
Logger.debug("Couldn't create subscription")
Logger.debug(inspect(reason))
{:error, reason}
end
end
def incoming_subscription_request(user, params) do
Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}")
{:error, "Invalid WebSub request"}
end
defp get_subscription(topic, callback) do
Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
%WebsubServerSubscription{}
end
# Temp hack for mastodon.
defp lease_time(%{"hub.lease_seconds" => ""}) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
{:ok, String.to_integer(lease_seconds)}
end
defp lease_time(_) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp valid_topic(%{"hub.topic" => topic}, user) do
if topic == OStatus.feed_path(user) do
{:ok, OStatus.feed_path(user)}
else
{:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
end
end
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
topic = subscribed.info.topic
# FIXME: Race condition, use transactions
{:ok, subscription} =
with subscription when not is_nil(subscription) <-
Repo.get_by(WebsubClientSubscription, topic: topic) do
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq()
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
Repo.update(change)
else
_e ->
subscription = %WebsubClientSubscription{
topic: topic,
hub: subscribed.info.hub,
subscribers: [subscriber.ap_id],
state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
user: subscribed
}
Repo.insert(subscription)
end
requester.(subscription)
end
def gather_feed_data(topic, getter \\ &HTTP.get/1) do
with {:ok, response} <- getter.(topic),
status when status in 200..299 <- response.status,
body <- response.body,
doc <- XML.parse_document(body),
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do
name = XML.string_from_xpath("/feed/author[1]/name", doc)
preferred_username = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
display_name = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
avatar = OStatus.make_avatar_object(doc)
bio = XML.string_from_xpath("/feed/author[1]/summary", doc)
{:ok,
%{
"uri" => uri,
"hub" => hub,
"nickname" => preferred_username || name,
"name" => display_name || name,
"host" => URI.parse(uri).host,
"avatar" => avatar,
"bio" => bio
}}
else
e ->
{:error, e}
end
end
def request_subscription(websub, poster \\ &HTTP.post/3, timeout \\ 10_000) do
data = [
"hub.mode": "subscribe",
"hub.topic": websub.topic,
"hub.secret": websub.secret,
"hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id)
]
# This checks once a second if we are confirmed yet
websub_checker = fn ->
helper = fn helper ->
:timer.sleep(1000)
websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
if websub, do: websub, else: helper.(helper)
end
helper.(helper)
end
task = Task.async(websub_checker)
with {:ok, %{status: 202}} <-
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
{:ok, websub} <- Task.yield(task, timeout) do
{:ok, websub}
else
e ->
Task.shutdown(task)
change = Ecto.Changeset.change(websub, %{state: "rejected"})
{:ok, websub} = Repo.update(change)
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
Logger.debug(fn -> "error: #{inspect(e)}" end)
{:error, websub}
end
end
def refresh_subscriptions(delta \\ 60 * 60 * 24) do
Logger.debug("Refreshing subscriptions")
cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta)
query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
subs = Repo.all(query)
Enum.each(subs, fn sub ->
Federator.request_subscription(sub)
end)
end
def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status: code}} when code in 200..299 <-
HTTP.post(
callback,
xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(callback)
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code}
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_reachable(callback)
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end)
{:error, response}
end
end
def gather_webfinger_links(%User{} = user) do
[
%{
"rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
end
def gather_nodeinfo_protocol_names, do: ["ostatus"]
end

View file

@ -1,20 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub.WebsubClientSubscription do
use Ecto.Schema
alias Pleroma.User
schema "websub_client_subscriptions" do
field(:topic, :string)
field(:secret, :string)
field(:valid_until, :naive_datetime_usec)
field(:state, :string)
field(:subscribers, {:array, :string}, default: [])
field(:hub, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
end

View file

@ -1,99 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub.WebsubController do
use Pleroma.Web, :controller
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Federator
alias Pleroma.Web.Websub
alias Pleroma.Web.Websub.WebsubClientSubscription
require Logger
plug(
Pleroma.Web.FederatingPlug
when action in [
:websub_subscription_request,
:websub_subscription_confirmation,
:websub_incoming
]
)
def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)
with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) do
conn
|> send_resp(202, "Accepted")
else
{:error, reason} ->
conn
|> send_resp(500, reason)
end
end
# TODO: Extract this into the Websub module
def websub_subscription_confirmation(
conn,
%{
"id" => id,
"hub.mode" => "subscribe",
"hub.challenge" => challenge,
"hub.topic" => topic
} = params
) do
Logger.debug("Got WebSub confirmation")
Logger.debug(inspect(params))
lease_seconds =
if params["hub.lease_seconds"] do
String.to_integer(params["hub.lease_seconds"])
else
# Guess 3 days
60 * 60 * 24 * 3
end
with %WebsubClientSubscription{} = websub <-
Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do
valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), lease_seconds)
change = Ecto.Changeset.change(websub, %{state: "accepted", valid_until: valid_until})
{:ok, _websub} = Repo.update(change)
conn
|> send_resp(200, challenge)
else
_e ->
conn
|> send_resp(500, "Error")
end
end
def websub_subscription_confirmation(conn, params) do
Logger.info("Invalid WebSub confirmation request: #{inspect(params)}")
conn
|> send_resp(500, "Invalid parameters")
end
def websub_incoming(conn, %{"id" => id}) do
with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")),
signature <- String.downcase(signature),
%WebsubClientSubscription{} = websub <- Repo.get(WebsubClientSubscription, id),
{:ok, body, _conn} = read_body(conn),
^signature <- Websub.sign(websub.secret, body) do
Federator.incoming_doc(body)
conn
|> send_resp(200, "OK")
else
_e ->
Logger.debug("Can't handle incoming subscription post")
conn
|> send_resp(500, "Error")
end
end
end

View file

@ -1,17 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub.WebsubServerSubscription do
use Ecto.Schema
schema "websub_server_subscriptions" do
field(:topic, :string)
field(:callback, :string)
field(:secret, :string)
field(:valid_until, :naive_datetime)
field(:state, :string)
timestamps()
end
end

View file

@ -8,10 +8,6 @@ defmodule Pleroma.Workers.ReceiverWorker do
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
@impl Oban.Worker
def perform(%{"op" => "incoming_doc", "body" => doc}, _job) do
Federator.perform(:incoming_doc, doc)
end
def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do
Federator.perform(:incoming_ap_doc, params)
end

View file

@ -1,26 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SubscriberWorker do
alias Pleroma.Repo
alias Pleroma.Web.Federator
alias Pleroma.Web.Websub
use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing"
@impl Oban.Worker
def perform(%{"op" => "refresh_subscriptions"}, _job) do
Federator.perform(:refresh_subscriptions)
end
def perform(%{"op" => "request_subscription", "websub_id" => websub_id}, _job) do
websub = Repo.get(Websub.WebsubClientSubscription, websub_id)
Federator.perform(:request_subscription, websub)
end
def perform(%{"op" => "verify_websub", "websub_id" => websub_id}, _job) do
websub = Repo.get(Websub.WebsubServerSubscription, websub_id)
Federator.perform(:verify_websub, websub)
end
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: version("1.0.0"),
version: version("1.1.50"),
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
@ -69,6 +69,7 @@ defmodule Pleroma.Mixfile do
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
@ -220,7 +221,10 @@ defmodule Pleroma.Mixfile do
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <- branch_name not in ["master", "HEAD"] do
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()

View file

@ -0,0 +1,22 @@
defmodule Pleroma.Repo.Migrations.CreateSafeJsonbSet do
use Ecto.Migration
alias Pleroma.User
def change do
execute("""
create or replace function safe_jsonb_set(target jsonb, path text[], new_value jsonb, create_missing boolean default true) returns jsonb as $$
declare
result jsonb;
begin
result := jsonb_set(target, path, coalesce(new_value, 'null'::jsonb), create_missing);
if result is NULL then
raise 'jsonb_set tried to wipe the object, please report this incindent to Pleroma bug tracker. https://git.pleroma.social/pleroma/pleroma/issues/new';
return target;
else
return result;
end if;
end;
$$ language plpgsql;
""")
end
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do
def change do
execute(
"update users set info = jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true"
"update users set info = safe_jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true"
)
end
end

View file

@ -0,0 +1,8 @@
defmodule Pleroma.Repo.Migrations.DropWebsubTables do
use Ecto.Migration
def change do
drop_if_exists(table(:websub_client_subscriptions))
drop_if_exists(table(:websub_server_subscriptions))
end
end

View file

@ -19,6 +19,7 @@
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
"invisible": "litepub:invisible",
"directMessage": "litepub:directMessage",
"listMessage": {
"@id": "litepub:listMessage",

View file

@ -35,11 +35,11 @@ detect_branch() {
if [ "$branch" = "develop" ]; then
echo "develop"
elif [ "$branch" = "" ]; then
echo "master"
echo "stable"
else
# Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols —
# if supporting releases for more branches, need to ensure they contain only these symbols.
echo "Releases are built only for master and develop branches" >&2
echo "Can't detect the branch automatically, please specify it by using the --branch option." >&2
exit 1
fi
}
@ -141,8 +141,8 @@ else
ACTION="$1"
shift
if [ "$(echo \"$1\" | grep \"^-\" >/dev/null)" = false ]; then
echo "$1" | grep "^-" >/dev/null
if [ $? -eq 1 ]; then
SUBACTION="$1"
shift
fi

View file

@ -23,6 +23,39 @@ defmodule Pleroma.Conversation.ParticipationTest do
assert %Pleroma.Conversation{} = participation.conversation
end
test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do
user = insert(:user)
other_user = insert(:user)
{:ok, _} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
[%{read: true}] = Participation.for_user(user)
[%{read: false} = participation] = Participation.for_user(other_user)
assert User.get_cached_by_id(user.id).info.unread_conversation_count == 0
assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 1
{:ok, _} =
CommonAPI.post(other_user, %{
"status" => "Hey @#{user.nickname}.",
"visibility" => "direct",
"in_reply_to_conversation_id" => participation.id
})
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
[%{read: false}] = Participation.for_user(user)
[%{read: true}] = Participation.for_user(other_user)
assert User.get_cached_by_id(user.id).info.unread_conversation_count == 1
assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 0
end
test "for a new conversation, it sets the recipents of the participation" do
user = insert(:user)
other_user = insert(:user)
@ -32,7 +65,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
[participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients)
@ -100,6 +133,20 @@ defmodule Pleroma.Conversation.ParticipationTest do
refute participation.read
end
test "it marks all the user's participations as read" do
user = insert(:user)
other_user = insert(:user)
participation1 = insert(:participation, %{read: false, user: user})
participation2 = insert(:participation, %{read: false, user: user})
participation3 = insert(:participation, %{read: false, user: other_user})
{:ok, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
assert Participation.get(participation1.id).read == true
assert Participation.get(participation2.id).read == true
assert Participation.get(participation3.id).read == false
end
test "gets all the participations for a user, ordered by updated at descending" do
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://shitposter.club/users/moonman","attachment":[],"attributedTo":"https://shitposter.club/users/moonman","cc":["https://shitposter.club/users/moonman/followers"],"content":"@<a href=\"https://shitposter.club/users/9655\" class=\"h-card mention\" title=\"Solidarity for Pigs\">neimzr4luzerz</a> @<a href=\"https://gs.smuglo.li/user/2326\" class=\"h-card mention\" title=\"Dolus_McHonest\">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English","context":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","conversation":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","id":"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment","inReplyTo":"tag:shitposter.club,2017-05-05:noticeId=2827849:objectType=comment","inReplyToStatusId":2827849,"published":"2017-05-05T08:51:48Z","sensitive":false,"summary":null,"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://shitposter.club/oauth/authorize","oauthRegistrationEndpoint":"https://shitposter.club/api/v1/apps","oauthTokenEndpoint":"https://shitposter.club/oauth/token","sharedInbox":"https://shitposter.club/inbox"},"followers":"https://shitposter.club/users/moonman/followers","following":"https://shitposter.club/users/moonman/following","icon":{"type":"Image","url":"https://shitposter.club/media/bda6e00074f6a02cbf32ddb0abec08151eb4c795e580927ff7ad638d00cde4c8.jpg?name=blob.jpg"},"id":"https://shitposter.club/users/moonman","image":{"type":"Image","url":"https://shitposter.club/media/4eefb90d-cdb2-2b4f-5f29-7612856a99d2/4eefb90d-cdb2-2b4f-5f29-7612856a99d2.jpeg"},"inbox":"https://shitposter.club/users/moonman/inbox","manuallyApprovesFollowers":false,"name":"Captain Howdy","outbox":"https://shitposter.club/users/moonman/outbox","preferredUsername":"moonman","publicKey":{"id":"https://shitposter.club/users/moonman#main-key","owner":"https://shitposter.club/users/moonman","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOTitJ19ZqcOZHwSXQUM\nJq9ip4GNblp83LgwG1t5c2h2iaI3fXMsB4EaEBs8XHsoSFyDeDNRSPE3mtVgOnWv\n1eaXWMDerBT06th6DrElD9k5IoEPtZRY4HtZa1xGnte7+6RjuPOzZ1fR9C8WxGgi\nwb9iOUMhazpo85fC3iKCAL5XhiuA3Nas57MDJgueeI9BF+2oFelFZdMSWwG96uch\niDfp8nfpkmzYI6SWbylObjm8RsfZbGTosLHwWyJPEITeYI/5M0XwJe9dgVI1rVNU\n52kplWOGTo1rm6V0AMHaYAd9RpiXxe8xt5OeranrsE/5LvEQUl0fz7SE36YmsOaH\nTwIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"EMAIL:shitposterclub@gmail.com<br>XMPP: moon@talk.shitposter.club<br>PRONOUNS: none of your business<br><br>Purported leftist kike piece of shit","tag":[],"type":"Person","url":"https://shitposter.club/users/moonman"}

View file

@ -0,0 +1,55 @@
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"Hashtag": "as:Hashtag",
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji"
}],
"id": "http://mastodon.example.org/users/admin",
"type": "Application",
"invisible": true,
"following": "http://mastodon.example.org/users/admin/following",
"followers": "http://mastodon.example.org/users/admin/followers",
"inbox": "http://mastodon.example.org/users/admin/inbox",
"outbox": "http://mastodon.example.org/users/admin/outbox",
"preferredUsername": "admin",
"name": null,
"summary": "\u003cp\u003e\u003c/p\u003e",
"url": "http://mastodon.example.org/@admin",
"manuallyApprovesFollowers": false,
"publicKey": {
"id": "http://mastodon.example.org/users/admin#main-key",
"owner": "http://mastodon.example.org/users/admin",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"attachment": [{
"type": "PropertyValue",
"name": "foo",
"value": "bar"
},
{
"type": "PropertyValue",
"name": "foo1",
"value": "bar1"
}
],
"endpoints": {
"sharedInbox": "http://mastodon.example.org/inbox"
},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
},
"image": {
"type": "Image",
"mediaType": "image/png",
"url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
}

View file

@ -24,13 +24,13 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
subject: [subject1],
action: "delete"
})
log = Repo.one(ModerationLog)
assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}"
end
test "logging user creation by moderator", %{
@ -128,7 +128,7 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
subject: [subject1],
action: "grant",
permission: "moderator"
})
@ -142,7 +142,7 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
subject: [subject1],
action: "revoke",
permission: "moderator"
})

View file

@ -65,7 +65,7 @@ defmodule Pleroma.Object.ContainmentTest do
assert capture_log(fn ->
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
end) =~
"[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}"
"[error] Could not decode user at fetch https://n1u.moe/users/rye"
end
end

View file

@ -27,31 +27,16 @@ defmodule Pleroma.Object.FetcherTest do
end
describe "actor origin containment" do
test_with_mock "it rejects objects with a bogus origin",
Pleroma.Web.OStatus,
[:passthrough],
[] do
test "it rejects objects with a bogus origin" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
test_with_mock "it rejects objects when attributedTo is wrong (variant 1)",
Pleroma.Web.OStatus,
[:passthrough],
[] do
test "it rejects objects when attributedTo is wrong (variant 1)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
test_with_mock "it rejects objects when attributedTo is wrong (variant 2)",
Pleroma.Web.OStatus,
[:passthrough],
[] do
test "it rejects objects when attributedTo is wrong (variant 2)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
end
@ -71,24 +56,6 @@ defmodule Pleroma.Object.FetcherTest do
assert object == object_again
end
test "it works with objects only available via Ostatus" do
{:ok, object} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
assert activity.data["id"]
{:ok, object_again} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
assert object == object_again
end
test "it correctly stitches up conversations between ostatus and ap" do
last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
{:ok, object} = Fetcher.fetch_object_from_id(last)
object = Object.get_by_ap_id(object.data["inReplyTo"])
assert object
end
end
describe "implementation quirks" do

View file

@ -0,0 +1,12 @@
defmodule Pleroma.SafeJsonbSetTest do
use Pleroma.DataCase
test "it doesn't wipe the object when asked to set the value to NULL" do
assert %{rows: [[%{"key" => "value", "test" => nil}]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);",
[]
)
end
end

View file

@ -69,8 +69,7 @@ defmodule Pleroma.SignatureTest do
test "it returns error when not found user" do
assert capture_log(fn ->
assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) ==
{:error, {:error, :ok}}
{:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id"))
end) =~ "[error] Could not decode user"
end
end

View file

@ -281,26 +281,6 @@ defmodule Pleroma.Factory do
}
end
def websub_subscription_factory do
%Pleroma.Web.Websub.WebsubServerSubscription{
topic: "http://example.org",
callback: "http://example.org/callback",
secret: "here's a secret",
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100),
state: "requested"
}
end
def websub_client_subscription_factory do
%Pleroma.Web.Websub.WebsubClientSubscription{
topic: "http://example.org",
secret: "here's a secret",
valid_until: nil,
state: "requested",
subscribers: []
}
end
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",

View file

@ -38,6 +38,14 @@ defmodule HttpRequestMock do
}}
end
def get("https://shitposter.club/users/moonman", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json")
}}
end
def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do
{:ok,
%Tesla.Env{
@ -340,6 +348,14 @@ defmodule HttpRequestMock do
}}
end
def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json")
}}
end
def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do
{:error, :nxdomain}
end
@ -620,7 +636,7 @@ defmodule HttpRequestMock do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.html")
body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json")
}}
end

View file

@ -65,21 +65,6 @@ defmodule Pleroma.UserSearchTest do
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
end
test "finds users, ranking by similarity" do
u1 = insert(:user, %{name: "lain"})
_u2 = insert(:user, %{name: "ean"})
u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
end
test "finds users, handling misspelled requests" do
u1 = insert(:user, %{name: "lain"})
assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
end
test "finds users, boosting ranks of friends and followers" do
u1 = insert(:user)
u2 = insert(:user, %{name: "Doe"})
@ -163,17 +148,6 @@ defmodule Pleroma.UserSearchTest do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
test "finds a user whose name is nil" do
_user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
assert user_two ==
User.search("lain@pleroma.soykaf.com")
|> List.first()
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
end
test "does not yield false-positive matches" do
insert(:user, %{name: "John Doe"})

View file

@ -190,23 +190,6 @@ defmodule Pleroma.UserTest do
refute User.following?(follower, followed)
end
# This is a somewhat useless test.
# test "following a remote user will ensure a websub subscription is present" do
# user = insert(:user)
# {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
# assert followed.local == false
# {:ok, user} = User.follow(user, followed)
# assert User.ap_followers(followed) in user.following
# query = from w in WebsubClientSubscription,
# where: w.topic == ^followed.info["topic"]
# websub = Repo.one(query)
# assert websub
# end
describe "unfollow/2" do
setup do
setting = Pleroma.Config.get([:instance, :external_user_synchronization])
@ -474,11 +457,6 @@ defmodule Pleroma.UserTest do
assert user == fetched_user
end
test "fetches an external user via ostatus if no user exists" do
{:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
assert fetched_user.nickname == "shp@social.heldscal.la"
end
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
@ -1254,6 +1232,20 @@ defmodule Pleroma.UserTest do
end
end
describe "invisible?/1" do
test "returns true for an invisible user" do
user = insert(:user, local: true, info: %{invisible: true})
assert User.invisible?(user)
end
test "returns false for a non-invisible user" do
user = insert(:user, local: true)
refute User.invisible?(user)
end
end
describe "visible_for?/2" do
test "returns true when the account is itself" do
user = insert(:user, local: true)

View file

@ -41,6 +41,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert called(Pleroma.Web.Streamer.stream("participation", participations))
end
end
test "streams them out on activity creation" do
user_one = insert(:user)
user_two = insert(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
{:ok, activity} =
CommonAPI.post(user_one, %{
"status" => "@#{user_two.nickname}",
"visibility" => "direct"
})
conversation =
activity.data["context"]
|> Pleroma.Conversation.get_for_ap_id()
|> Repo.preload(participations: :user)
assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
end
end
end
describe "fetching restricted by visibility" do
@ -87,6 +108,66 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end
end
describe "fetching excluded by visibility" do
test "it excludes by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "direct",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
refute direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "unlisted",
"actor_id" => user.ap_id
})
assert public_activity in activities
refute unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "private",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
refute private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "public",
"actor_id" => user.ap_id
})
refute public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
@ -98,6 +179,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
test "it returns a user that is invisible" do
user_id = "http://mastodon.example.org/users/relay"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert User.invisible?(user)
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
@ -19,11 +20,16 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
end
test "relay actor is invisible" do
user = Relay.get_actor()
assert User.invisible?(user)
end
describe "follow/1" do
test "returns errors when user not found" do
assert capture_log(fn ->
assert Relay.follow("test-ap-id") == {:error, "Could not fetch by AP id"}
end) =~ "Could not fetch by AP id"
{:error, _} = Relay.follow("test-ap-id")
end) =~ "Could not decode user at fetch"
end
test "returns activity" do
@ -41,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
describe "unfollow/1" do
test "returns errors when user not found" do
assert capture_log(fn ->
assert Relay.unfollow("test-ap-id") == {:error, "Could not fetch by AP id"}
end) =~ "Could not fetch by AP id"
{:error, _} = Relay.unfollow("test-ap-id")
end) =~ "Could not decode user at fetch"
end
test "returns activity" do

View file

@ -7,14 +7,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub.WebsubClientSubscription
import Mock
import Pleroma.Factory
@ -1181,32 +1178,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it translates ostatus IDs to external URLs" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
{:ok, [referent_activity]} = OStatus.handle_incoming(incoming)
user = insert(:user)
{:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
end
test "it translates ostatus reply_to IDs to external URLs" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
{:ok, [referred_activity]} = OStatus.handle_incoming(incoming)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
end
test "it strips internal hashtag data" do
user = insert(:user)
@ -1371,21 +1342,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
end
end
describe "maybe_retire_websub" do
test "it deletes all websub client subscripitions with the user as topic" do
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
{:ok, ws} = Repo.insert(subscription)
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
{:ok, ws2} = Repo.insert(subscription)
Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
refute Repo.get(WebsubClientSubscription, ws.id)
assert Repo.get(WebsubClientSubscription, ws2.id)
end
end
describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{

View file

@ -76,6 +76,12 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert result["image"]["url"] == "https://somebanner"
end
test "renders an invisible user with the invisible property set to true" do
user = insert(:user, %{info: %{invisible: true}})
assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
end
describe "endpoints" do
test "local users have a usable endpoints structure" do
user = insert(:user)

View file

@ -17,8 +17,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.Web.MediaProxy
import Pleroma.Factory
describe "/api/pleroma/admin/users" do
test "Delete" do
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "DELETE /api/pleroma/admin/users" do
test "single user" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@ -30,15 +36,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
log_entry = Repo.one(ModerationLog)
assert log_entry.data["subject"]["nickname"] == user.nickname
assert log_entry.data["action"] == "delete"
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted user @#{user.nickname}"
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == user.nickname
end
test "multiple users" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user)
user_two = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users", %{
nicknames: [user_one.nickname, user_two.nickname]
})
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
response = json_response(conn, 200)
assert response -- [user_one.nickname, user_two.nickname] == []
end
end
describe "/api/pleroma/admin/users" do
test "Create" do
admin = insert(:user, info: %{is_admin: true})
@ -404,6 +431,29 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} made @#{user.nickname} admin"
end
test "/:right POST, can add to a permission group (multiple)" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user)
user_two = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
assert json_response(conn, 200) == %{
"is_admin" => true
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
test "/:right DELETE, can remove from a permission group" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user, info: %{is_admin: true})
@ -423,63 +473,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
end
describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
setup %{conn: conn} do
test "/:right DELETE, can remove from a permission group (multiple)" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user, info: %{is_admin: true})
user_two = insert(:user, info: %{is_admin: true})
conn =
conn
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})
%{conn: conn, admin: admin}
end
test "deactivates the user", %{conn: conn, admin: admin} do
user = insert(:user)
conn =
conn
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
user = User.get_cached_by_id(user.id)
assert user.info.deactivated == true
assert json_response(conn, :no_content)
assert json_response(conn, 200) == %{
"is_admin" => false
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}"
end
test "activates the user", %{conn: conn, admin: admin} do
user = insert(:user, info: %{deactivated: true})
conn =
conn
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
user = User.get_cached_by_id(user.id)
assert user.info.deactivated == false
assert json_response(conn, :no_content)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} activated user @#{user.nickname}"
end
test "returns 403 when requested by a non-admin", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
assert json_response(conn, :forbidden)
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
user_two.nickname
}"
end
end
@ -1029,6 +1046,50 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
test "PATCH /api/pleroma/admin/users/activate" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user, info: %{deactivated: true})
user_two = insert(:user, info: %{deactivated: true})
conn =
build_conn()
|> assign(:user, admin)
|> patch(
"/api/pleroma/admin/users/activate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/deactivate" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user, info: %{deactivated: false})
user_two = insert(:user, info: %{deactivated: false})
conn =
build_conn()
|> assign(:user, admin)
|> patch(
"/api/pleroma/admin/users/deactivate",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@ -1053,7 +1114,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}"
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
describe "POST /api/pleroma/admin/users/invite_token" do
@ -2486,6 +2547,74 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert User.get_by_id(user.id).info.password_reset_pending == true
end
end
describe "relays" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin), admin: admin}
end
test "POST /relay", %{admin: admin} do
conn =
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
end
test "GET /relay", %{admin: admin} do
Pleroma.Web.ActivityPub.Relay.get_actor()
|> Ecto.Changeset.change(
following: [
"http://test-app.com/user/test1",
"http://test-app.com/user/test1",
"http://test-app-42.com/user/test1"
]
)
|> Pleroma.User.update_and_set_cache()
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/relay")
assert json_response(conn, 200)["relays"] -- ["test-app.com", "test-app-42.com"] == []
end
test "DELETE /relay", %{admin: admin} do
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
conn =
build_conn()
|> assign(:user, admin)
|> delete("/api/pleroma/admin/relay", %{
relay_url: "http://mastodon.example.org/users/admin"
})
assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
[log_entry_one, log_entry_two] = Repo.all(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry_one) ==
"@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
assert ModerationLog.get_log_entry_message(log_entry_two) ==
"@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
end
end
end
# Needed for testing

View file

@ -111,93 +111,6 @@ defmodule Pleroma.Web.FederatorTest do
all_enqueued(worker: PublisherWorker)
)
end
test "it federates only to reachable instances via Websub" do
user = insert(:user)
websub_topic = Pleroma.Web.OStatus.feed_path(user)
sub1 =
insert(:websub_subscription, %{
topic: websub_topic,
state: "active",
callback: "http://pleroma.soykaf.com/cb"
})
sub2 =
insert(:websub_subscription, %{
topic: websub_topic,
state: "active",
callback: "https://pleroma2.soykaf.com/cb"
})
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(sub2.callback, dt)
Instances.set_consistently_unreachable(sub1.callback)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
expected_callback = sub2.callback
expected_dt = NaiveDateTime.to_iso8601(dt)
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
assert ObanHelpers.member?(
%{
"op" => "publish_one",
"params" => %{
"callback" => expected_callback,
"unreachable_since" => expected_dt
}
},
all_enqueued(worker: PublisherWorker)
)
end
test "it federates only to reachable instances via Salmon" do
user = insert(:user)
_remote_user1 =
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
info: %{salmon: "https://domain.com/salmon"}
})
remote_user2 =
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
info: %{salmon: "https://domain2.com/salmon"}
})
remote_user2_id = remote_user2.id
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(remote_user2.ap_id, dt)
Instances.set_consistently_unreachable("domain.com")
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
expected_dt = NaiveDateTime.to_iso8601(dt)
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
assert ObanHelpers.member?(
%{
"op" => "publish_one",
"params" => %{
"recipient_id" => remote_user2_id,
"unreachable_since" => expected_dt
}
},
all_enqueued(worker: PublisherWorker)
)
end
end
describe "Receive an activity" do

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