diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f071b7e2..2fdcb014a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - NodeInfo: Return `mailerEnabled` in `metadata` - Mastodon API: Unsubscribe followers when they unfollow a user - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) +- Improve digest email template ### Fixed - Not being able to pin unlisted posts - Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised. +- Favorites timeline doing database-intensive queries - Metadata rendering errors resulting in the entire page being inaccessible - `federation_incoming_replies_max_depth` option being ignored in certain cases - Federation/MediaProxy not working with instances that have wrong certificate order @@ -43,9 +45,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 - MRF: ensure that subdomain_match calls are case-insensitive +- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances - MRF: fix use of unserializable keyword lists in describe() implementations +- ActivityPub: Deactivated user deletion ### Added +- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically. +- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour. +- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty. +- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default. +- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data. - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo. Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules. - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) @@ -66,6 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add `pleroma.deactivated` to the Account entity - 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 - 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 @@ -84,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Relays: Added a task to list relay subscriptions. - Mix Tasks: `mix pleroma.database fix_likes_collections` - Federation: Remove `likes` from objects. +- Admin API: Added moderation log ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text @@ -185,6 +196,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - Admin API: Move the user related API to `api/pleroma/admin/users` +- Admin API: `POST /api/pleroma/admin/users` will take list of users - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` diff --git a/config/config.exs b/config/config.exs index 758661120..f630771a3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -255,6 +255,10 @@ config :pleroma, :instance, dynamic_configuration: false, user_bio_length: 5000, user_name_length: 100, + max_account_fields: 10, + max_remote_account_fields: 20, + account_field_name_length: 512, + account_field_value_length: 512, external_user_synchronization: true config :pleroma, :markup, @@ -452,6 +456,7 @@ config :pleroma, Pleroma.Web.Federator.RetryQueue, max_retries: 5 config :pleroma_job_queue, :queues, + activity_expiration: 10, federator_incoming: 50, federator_outgoing: 50, web_push: 50, @@ -512,6 +517,17 @@ config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false +config :pleroma, Pleroma.Emails.UserEmail, + logo: nil, + styling: %{ + link_color: "#d8a070", + background_color: "#2C3645", + content_background_color: "#1B2635", + header_color: "#d8a070", + text_color: "#b9b9ba", + text_muted_color: "#b9b9ba" + } + config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics" config :pleroma, Pleroma.ScheduledActivity, @@ -540,15 +556,9 @@ config :pleroma, :env, Mix.env() config :http_signatures, adapter: Pleroma.Signature -config :pleroma, :rate_limit, - search: [{1000, 10}, {1000, 30}], - app_account_creation: {1_800_000, 25}, - relations_actions: {10_000, 10}, - relation_id_action: {60_000, 2}, - statuses_actions: {10_000, 15}, - status_id_action: {60_000, 3}, - password_reset: {1_800_000, 5}, - account_confirmation_resend: {8_640_000, 5} +config :pleroma, :rate_limit, nil + +config :pleroma, Pleroma.ActivityExpiration, enabled: true # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/config/test.exs b/config/test.exs index 6f75f39b5..567780987 100644 --- a/config/test.exs +++ b/config/test.exs @@ -71,7 +71,8 @@ config :pleroma, Pleroma.ScheduledActivity, config :pleroma, :rate_limit, search: [{1000, 30}, {1000, 30}], app_account_creation: {10_000, 5}, - password_reset: {1000, 30} + password_reset: {1000, 30}, + ap_routes: nil config :pleroma, :http_security, report_uri: "https://endpoint.com" @@ -85,11 +86,10 @@ config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp3 config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock -try do +if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" -rescue - _ -> - IO.puts( - "You may want to create test.secret.exs to declare custom database connection parameters." - ) +else + IO.puts( + "You may want to create test.secret.exs to declare custom database connection parameters." + ) end diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 7ccb90836..d79c342be 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -694,3 +694,27 @@ Compile time settings (need instance reboot): ] } ``` + +## `/api/pleroma/admin/moderation_log` +### Get moderation log +- Method `GET` +- Params: + - *optional* `page`: **integer** page number + - *optional* `page_size`: **integer** number of users per page (default is `50`) +- Response: + +```json +[ + { + "data": { + "actor": { + "id": 1, + "nickname": "lain" + }, + "action": "relay_follow" + }, + "time": 1502812026, // timestamp + "message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message + } +] +``` diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 1907d70c8..f34e3dd72 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -25,6 +25,7 @@ Has these additional fields under the `pleroma` object: - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` +- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire ## Attachments @@ -59,12 +60,19 @@ Has these additional fields under the `pleroma` object: - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API +## Conversations + +Has an additional field under the `pleroma` object: + +- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation. + ## Account Search Behavior has changed: - `/api/v1/accounts/search`: Does not require authentication + ## Notifications Has these additional fields under the `pleroma` object: @@ -79,6 +87,8 @@ Additional parameters can be added to the JSON body/Form data: - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. +- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour. +- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. ## PATCH `/api/v1/update_credentials` diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 5698e88ac..b134b31a8 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -319,3 +319,38 @@ See [Admin-API](Admin-API.md) "healthy": true # Instance state } ``` + +# Pleroma Conversations + +Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints: + +1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user. +2. Pleroma Conversations statuses can be requested by Conversation id. +3. Pleroma Conversations can be replied to. + +Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation. + +The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation. + + +## `GET /api/v1/pleroma/conversations/:id/statuses` +### Timeline for a given conversation +* Method `GET` +* Authentication: required +* Params: Like other timelines +* Response: JSON, statuses (200 - healthy, 503 unhealthy). + +## `GET /api/v1/pleroma/conversations/:id` +### The conversation with the given ID. +* Method `GET` +* Authentication: required +* Params: None +* Response: JSON, statuses (200 - healthy, 503 unhealthy). + +## `PATCH /api/v1/pleroma/conversations/:id` +### Update a conversation. Used to change the set of recipients. +* Method `PATCH` +* Authentication: required +* Params: + * `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) diff --git a/docs/config.md b/docs/config.md index 20311db54..7a8819c91 100644 --- a/docs/config.md +++ b/docs/config.md @@ -8,7 +8,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw * `filters`: List of `Pleroma.Upload.Filter` to use. * `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe` * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host. -* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. +* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. @@ -132,6 +132,10 @@ config :pleroma, Pleroma.Emails.Mailer, * `skip_thread_containment`: Skip filter out broken threads. The default is `false`. * `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`. * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api. +* `max_account_fields`: The maximum number of custom fields in the user profile (default: `10`) +* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`) +* `account_field_name_length`: An account field name maximum length (default: `512`) +* `account_field_value_length`: An account field value maximum length (default: `512`) * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. @@ -491,6 +495,10 @@ config :auto_linker, * `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`) * `enabled`: whether scheduled activities are sent to the job queue to be executed +## Pleroma.ActivityExpiration + +# `enabled`: whether expired activities will be sent to the job queue to be deleted + ## Pleroma.Web.Auth.Authenticator * `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator @@ -555,6 +563,11 @@ Email notifications settings. - interval: Minimum interval between digest emails to one user - inactivity_threshold: Minimum user inactivity threshold +## Pleroma.Emails.UserEmail + +- `:logo` - a path to a custom logo. Set it to `nil` to use the default Pleroma logo. +- `:styling` - a map with color settings for email templates. + ## OAuth consumer mode OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). @@ -658,6 +671,8 @@ This will probably take a long time. ## :rate_limit +This is an advanced feature and disabled by default. + A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where: * The first element: `scale` (Integer). The time scale in milliseconds. diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 81c207e10..430116a50 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -27,7 +27,15 @@ defmodule Mix.Tasks.Pleroma.Digest do patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at} - _user = Pleroma.DigestEmailWorker.perform(patched_user) - Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") + with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do + {:ok, _} = Pleroma.Emails.Mailer.deliver(email) + + Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") + else + _ -> + Mix.shell().info( + "Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}" + ) + end end end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index c7324fff6..a738fae75 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -53,13 +53,11 @@ defmodule Mix.Tasks.Pleroma.Relay do def run(["list"]) do start_pleroma() - with %User{} = user <- Relay.get_actor() do - user.following - |> Enum.each(fn entry -> - URI.parse(entry) - |> Map.get(:host) - |> shell_info() - end) + 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)) else e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") end diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index baf1e7722..2d4e9da0c 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Activity do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object @@ -59,6 +60,8 @@ defmodule Pleroma.Activity do # typical case. has_one(:object, Object, on_delete: :nothing, foreign_key: :id) + has_one(:expiration, ActivityExpiration, on_delete: :delete_all) + timestamps() end @@ -96,6 +99,7 @@ defmodule Pleroma.Activity do from([a] in query, left_join: tm in ThreadMute, on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data), + as: :thread_mute, select: %Activity{a | thread_muted?: not is_nil(tm.id)} ) end diff --git a/lib/pleroma/activity_expiration.ex b/lib/pleroma/activity_expiration.ex new file mode 100644 index 000000000..bf57abca4 --- /dev/null +++ b/lib/pleroma/activity_expiration.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpiration do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.FlakeId + alias Pleroma.Repo + + import Ecto.Changeset + import Ecto.Query + + @type t :: %__MODULE__{} + @min_activity_lifetime :timer.hours(1) + + schema "activity_expirations" do + belongs_to(:activity, Activity, type: FlakeId) + field(:scheduled_at, :naive_datetime) + end + + def changeset(%ActivityExpiration{} = expiration, attrs) do + expiration + |> cast(attrs, [:scheduled_at]) + |> validate_required([:scheduled_at]) + |> validate_scheduled_at() + end + + def get_by_activity_id(activity_id) do + ActivityExpiration + |> where([exp], exp.activity_id == ^activity_id) + |> Repo.one() + end + + def create(%Activity{} = activity, scheduled_at) do + %ActivityExpiration{activity_id: activity.id} + |> changeset(%{scheduled_at: scheduled_at}) + |> Repo.insert() + end + + def due_expirations(offset \\ 0) do + naive_datetime = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(offset, :millisecond) + + ActivityExpiration + |> where([exp], exp.scheduled_at < ^naive_datetime) + |> Repo.all() + end + + def validate_scheduled_at(changeset) do + validate_change(changeset, :scheduled_at, fn _, scheduled_at -> + if not expires_late_enough?(scheduled_at) do + [scheduled_at: "an ephemeral activity must live for at least one hour"] + else + [] + end + end) + end + + def expires_late_enough?(scheduled_at) do + now = NaiveDateTime.utc_now() + diff = NaiveDateTime.diff(scheduled_at, now, :millisecond) + diff >= @min_activity_lifetime + end +end diff --git a/lib/pleroma/activity_expiration_worker.ex b/lib/pleroma/activity_expiration_worker.ex new file mode 100644 index 000000000..0f9e715f8 --- /dev/null +++ b/lib/pleroma/activity_expiration_worker.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationWorker do + alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + require Logger + use GenServer + import Ecto.Query + + @schedule_interval :timer.minutes(1) + + def start_link(_) do + GenServer.start_link(__MODULE__, nil) + end + + @impl true + def init(_) do + if Config.get([ActivityExpiration, :enabled]) do + schedule_next() + {:ok, nil} + else + :ignore + end + end + + def perform(:execute, expiration_id) do + try do + expiration = + ActivityExpiration + |> where([e], e.id == ^expiration_id) + |> Repo.one!() + + activity = Activity.get_by_id_with_object(expiration.activity_id) + user = User.get_by_ap_id(activity.object.data["actor"]) + CommonAPI.delete(activity.id, user) + rescue + error -> + Logger.error("#{__MODULE__} Couldn't delete expired activity: #{inspect(error)}") + end + end + + @impl true + def handle_info(:perform, state) do + ActivityExpiration.due_expirations(@schedule_interval) + |> Enum.each(fn expiration -> + PleromaJobQueue.enqueue(:activity_expiration, __MODULE__, [:execute, expiration.id]) + end) + + schedule_next() + {:noreply, state} + end + + defp schedule_next do + Process.send_after(self(), :perform, @schedule_interval) + end +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 25e56b9e2..483ac1f39 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -35,7 +35,8 @@ defmodule Pleroma.Application do Pleroma.Emoji, Pleroma.Captcha, Pleroma.FlakeId, - Pleroma.ScheduledActivityWorker + Pleroma.ScheduledActivityWorker, + Pleroma.ActivityExpirationWorker ] ++ cachex_children() ++ hackney_pool_children() ++ diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index bc97b39ca..be5821ad7 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Conversation do alias Pleroma.Conversation.Participation + alias Pleroma.Conversation.Participation.RecipientShip alias Pleroma.Repo alias Pleroma.User use Ecto.Schema @@ -39,6 +40,15 @@ defmodule Pleroma.Conversation do Repo.get_by(__MODULE__, ap_id: ap_id) end + def maybe_create_recipientships(participation, activity) do + participation = Repo.preload(participation, :recipients) + + if participation.recipients |> Enum.empty?() do + recipients = User.get_all_by_ap_id(activity.recipients) + RecipientShip.create(recipients, participation) + end + end + @doc """ This will 1. Create a conversation if there isn't one already @@ -60,6 +70,7 @@ defmodule Pleroma.Conversation do {:ok, participation} = Participation.create_for_user_and_conversation(user, conversation, opts) + maybe_create_recipientships(participation, activity) participation end) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 5883e4183..ea5b9fe17 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Conversation.Participation do use Ecto.Schema alias Pleroma.Conversation + alias Pleroma.Conversation.Participation.RecipientShip alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -17,6 +18,9 @@ defmodule Pleroma.Conversation.Participation do field(:read, :boolean, default: false) field(:last_activity_id, Pleroma.FlakeId, virtual: true) + has_many(:recipient_ships, RecipientShip) + has_many(:recipients, through: [:recipient_ships, :user]) + timestamps() end @@ -65,6 +69,14 @@ defmodule Pleroma.Conversation.Participation do |> Pleroma.Pagination.fetch_paginated(params) end + def for_user_and_conversation(user, conversation) do + from(p in __MODULE__, + where: p.user_id == ^user.id, + where: p.conversation_id == ^conversation.id + ) + |> Repo.one() + end + def for_user_with_last_activity_id(user, params \\ %{}) do for_user(user, params) |> Enum.map(fn participation -> @@ -81,4 +93,46 @@ defmodule Pleroma.Conversation.Participation do end) |> Enum.filter(& &1.last_activity_id) end + + def get(_, _ \\ []) + def get(nil, _), do: nil + + def get(id, params) do + query = + if preload = params[:preload] do + from(p in __MODULE__, + preload: ^preload + ) + else + __MODULE__ + end + + Repo.get(query, id) + end + + def set_recipients(participation, user_ids) do + user_ids = + [participation.user_id | user_ids] + |> Enum.uniq() + + Repo.transaction(fn -> + query = + from(r in RecipientShip, + where: r.participation_id == ^participation.id + ) + + Repo.delete_all(query) + + users = + from(u in User, + where: u.id in ^user_ids + ) + |> Repo.all() + + RecipientShip.create(users, participation) + :ok + end) + + {:ok, Repo.preload(participation, :recipients, force: true)} + end end diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex new file mode 100644 index 000000000..932cbd04c --- /dev/null +++ b/lib/pleroma/conversation/participation_recipient_ship.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation.RecipientShip do + use Ecto.Schema + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Changeset + + schema "conversation_participation_recipient_ships" do + belongs_to(:user, User, type: Pleroma.FlakeId) + belongs_to(:participation, Participation) + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :participation_id]) + |> validate_required([:user_id, :participation_id]) + end + + def create(%User{} = user, participation), do: create([user], participation) + + def create(users, participation) do + Enum.each(users, fn user -> + %__MODULE__{} + |> creation_cng(%{user_id: user.id, participation_id: participation.id}) + |> Repo.insert!() + end) + end +end diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index 18e67d39b..5644d6a67 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.DigestEmailWorker do import Ecto.Query diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 49046bb8b..40b67ff56 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -7,21 +7,21 @@ defmodule Pleroma.Emails.UserEmail do use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email} + alias Pleroma.Config + alias Pleroma.User alias Pleroma.Web.Endpoint alias Pleroma.Web.Router - defp instance_config, do: Pleroma.Config.get(:instance) - - defp instance_name, do: instance_config()[:name] + defp instance_name, do: Config.get([:instance, :name]) defp sender do - email = Keyword.get(instance_config(), :notify_email, instance_config()[:email]) + email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email]) {instance_name(), email} end defp recipient(email, nil), do: email defp recipient(email, name), do: {name, email} - defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name) + defp recipient(%User{} = user), do: recipient(user.email, user.name) def password_reset_email(user, token) when is_binary(token) do password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token) @@ -93,67 +93,86 @@ defmodule Pleroma.Emails.UserEmail do Includes Mentions and New Followers data If there are no mentions (even when new followers exist), the function will return nil """ - @spec digest_email(Pleroma.User.t()) :: Swoosh.Email.t() | nil + @spec digest_email(User.t()) :: Swoosh.Email.t() | nil def digest_email(user) do - new_notifications = - Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at) - |> Enum.reduce(%{followers: [], mentions: []}, fn - %{activity: %{data: %{"type" => "Create"}, actor: actor} = activity} = notification, - acc -> - new_mention = %{ - data: notification, - object: Pleroma.Object.normalize(activity), - from: Pleroma.User.get_by_ap_id(actor) - } + notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at) - %{acc | mentions: [new_mention | acc.mentions]} + mentions = + notifications + |> Enum.filter(&(&1.activity.data["type"] == "Create")) + |> Enum.map(fn notification -> + object = Pleroma.Object.normalize(notification.activity) + object = update_in(object.data["content"], &format_links/1) - %{activity: %{data: %{"type" => "Follow"}, actor: actor} = activity} = notification, - acc -> - new_follower = %{ - data: notification, - object: Pleroma.Object.normalize(activity), - from: Pleroma.User.get_by_ap_id(actor) - } - - %{acc | followers: [new_follower | acc.followers]} - - _, acc -> - acc + %{ + data: notification, + object: object, + from: User.get_by_ap_id(notification.activity.actor) + } end) - with [_ | _] = mentions <- new_notifications.mentions do + followers = + notifications + |> Enum.filter(&(&1.activity.data["type"] == "Follow")) + |> Enum.map(fn notification -> + %{ + data: notification, + object: Pleroma.Object.normalize(notification.activity), + from: User.get_by_ap_id(notification.activity.actor) + } + end) + + unless Enum.empty?(mentions) do + styling = Config.get([__MODULE__, :styling]) + logo = Config.get([__MODULE__, :logo]) + html_data = %{ instance: instance_name(), user: user, mentions: mentions, - followers: new_notifications.followers, - unsubscribe_link: unsubscribe_url(user, "digest") + followers: followers, + unsubscribe_link: unsubscribe_url(user, "digest"), + styling: styling } + logo_path = + if is_nil(logo) do + Path.join(:code.priv_dir(:pleroma), "static/static/logo.png") + else + Path.join(Config.get([:instance, :static_dir]), logo) + end + new() |> to(recipient(user)) |> from(sender()) |> subject("Your digest from #{instance_name()}") + |> put_layout(false) |> render_body("digest.html", html_data) - else - _ -> - nil + |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline)) end end + defp format_links(str) do + re = ~r//iU + %{link_color: color} = Config.get([__MODULE__, :styling]) + + Regex.replace(re, str, fn link -> + String.replace(link, " user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false} |> Pleroma.JWT.generate_and_sign!() |> Base.encode64() - Router.Helpers.subscription_url(Pleroma.Web.Endpoint, :unsubscribe, token) + Router.Helpers.subscription_url(Endpoint, :unsubscribe, token) end end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 06e60cba3..3951f0f51 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -282,3 +282,31 @@ defmodule Pleroma.HTML.Transform.MediaProxy do def scrub({_tag, children}), do: children def scrub(text), do: text end + +defmodule Pleroma.HTML.Scrubber.LinksOnly do + @moduledoc """ + An HTML scrubbing policy which limits to links only. + """ + + @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) + + require HtmlSanitizeEx.Scrubber.Meta + alias HtmlSanitizeEx.Scrubber.Meta + + Meta.remove_cdata_sections_before_scrub() + Meta.strip_comments() + + # links + Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) + + Meta.allow_tag_with_this_attribute_values("a", "rel", [ + "tag", + "nofollow", + "noopener", + "noreferrer", + "me" + ]) + + Meta.allow_tag_with_these_attributes("a", ["name", "title"]) + Meta.strip_everything_not_covered() +end diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex new file mode 100644 index 000000000..1ef6fe67a --- /dev/null +++ b/lib/pleroma/moderation_log.ex @@ -0,0 +1,433 @@ +defmodule Pleroma.ModerationLog do + use Ecto.Schema + + alias Pleroma.Activity + alias Pleroma.ModerationLog + alias Pleroma.Repo + alias Pleroma.User + + import Ecto.Query + + schema "moderation_log" do + field(:data, :map) + + timestamps() + end + + def get_all(page, page_size) do + from(q in __MODULE__, + order_by: [desc: q.inserted_at], + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + |> Repo.all() + end + + def insert_log(%{ + actor: %User{} = actor, + subject: %User{} = subject, + action: action, + permission: permission + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + subject: user_to_map(subject), + action: action, + permission: permission + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_update", + subject: %Activity{data: %{"type" => "Flag"}} = subject + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_update", + subject: report_to_map(subject) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "report_response", + subject: %Activity{} = subject, + text: text + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "report_response", + subject: report_to_map(subject), + text: text + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_update", + subject: %Activity{} = subject, + sensitive: sensitive, + visibility: visibility + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_update", + subject: status_to_map(subject), + sensitive: sensitive, + visibility: visibility + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: "status_delete", + subject_id: subject_id + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "status_delete", + subject_id: subject_id + } + }) + end + + @spec insert_log(%{actor: User, subject: User, action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subject: user_to_map(subject) + } + }) + end + + @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: + {:ok, ModerationLog} | {:error, any} + def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do + subjects = Enum.map(subjects, &user_to_map/1) + + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + subjects: subjects + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "follow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "follow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + followed: %User{} = followed, + follower: %User{} = follower, + action: "unfollow" + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: "unfollow", + followed: user_to_map(followed), + follower: user_to_map(follower) + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + nicknames: nicknames, + tags: tags, + action: action + }) do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + nicknames: nicknames, + tags: tags, + action: action + } + }) + end + + def insert_log(%{ + actor: %User{} = actor, + action: action, + target: target + }) + when action in ["relay_follow", "relay_unfollow"] do + Repo.insert(%ModerationLog{ + data: %{ + actor: user_to_map(actor), + action: action, + target: target + } + }) + end + + defp user_to_map(%User{} = user) do + user + |> Map.from_struct() + |> Map.take([:id, :nickname]) + |> Map.put(:type, "user") + end + + defp report_to_map(%Activity{} = report) do + %{ + type: "report", + id: report.id, + state: report.data["state"] + } + end + + defp status_to_map(%Activity{} = status) do + %{ + type: "status", + id: status.id + } + end + + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => action, + "followed" => %{"nickname" => followed_nickname}, + "follower" => %{"nickname" => follower_nickname} + } + }) do + "@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "delete", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deleted user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "create", + "subjects" => subjects + } + }) do + nicknames = + subjects + |> Enum.map(&"@#{&1["nickname"]}") + |> Enum.join(", ") + + "@#{actor_nickname} created users: #{nicknames}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "activate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} activated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "deactivate", + "subject" => %{"nickname" => subject_nickname, "type" => "user"} + } + }) do + "@#{actor_nickname} deactivated user @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "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}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "nicknames" => nicknames, + "tags" => tags, + "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}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "grant", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} made @#{subject_nickname} #{permission}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "revoke", + "subject" => %{"nickname" => subject_nickname}, + "permission" => permission + } + }) do + "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_follow", + "target" => target + } + }) do + "@#{actor_nickname} followed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "relay_unfollow", + "target" => target + } + }) do + "@#{actor_nickname} unfollowed relay: #{target}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_update", + "subject" => %{"id" => subject_id, "state" => state, "type" => "report"} + } + }) do + "@#{actor_nickname} updated report ##{subject_id} with '#{state}' state" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "report_response", + "subject" => %{"id" => subject_id, "type" => "report"}, + "text" => text + } + }) do + "@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => nil, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => nil + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_update", + "subject" => %{"id" => subject_id, "type" => "status"}, + "sensitive" => sensitive, + "visibility" => visibility + } + }) do + "@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{ + visibility + }'" + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "status_delete", + "subject_id" => subject_id + } + }) do + "@#{actor_nickname} deleted status ##{subject_id}" + end +end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 8d79ddb1f..c1795ae0f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -117,9 +117,7 @@ defmodule Pleroma.Object.Fetcher do def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do Logger.info("Fetching object #{id} via AP") - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + date = Pleroma.Signature.signed_date() headers = [{:Accept, "application/activity+json"}] diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 1f98f215c..03efad30a 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -109,7 +109,11 @@ defmodule Pleroma.ReverseProxy do end with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts), - :ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do + :ok <- + header_length_constraint( + headers, + Keyword.get(opts, :max_body_length, @max_body_length) + ) do response(conn, client, url, code, headers, opts) else {:ok, code, headers} -> @@ -200,7 +204,11 @@ defmodule Pleroma.ReverseProxy do {:ok, data} <- client().stream_body(client), {:ok, duration} <- increase_read_duration(duration), sent_so_far = sent_so_far + byte_size(data), - :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), + :ok <- + body_size_constraint( + sent_so_far, + Keyword.get(opts, :max_body_length, @max_body_length) + ), {:ok, conn} <- chunk(conn, data) do chunk_reply(conn, client, opts, sent_so_far, duration) else diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 15bf3c317..f20aeb0d5 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -53,4 +53,10 @@ defmodule Pleroma.Signature do HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) end end + + def signed_date, do: signed_date(NaiveDateTime.utc_now()) + + def signed_date(%NaiveDateTime{} = date) do + Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index a1040fe71..29fd6d2ea 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -21,6 +21,7 @@ defmodule Pleroma.User do alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.OAuth alias Pleroma.Web.OStatus @@ -221,12 +222,12 @@ defmodule Pleroma.User do |> validate_length(:name, min: 1, max: name_limit) end - def upgrade_changeset(struct, params \\ %{}) do + def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) - info_cng = User.Info.user_upgrade(struct.info, params[:info]) + info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?) struct |> cast(params, [ @@ -329,7 +330,13 @@ defmodule Pleroma.User do @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)" def register(%Ecto.Changeset{} = changeset) do with {:ok, user} <- Repo.insert(changeset), - {:ok, user} <- autofollow_users(user), + {:ok, user} <- post_register_action(user) do + {:ok, user} + end + end + + def post_register_action(%User{} = user) do + with {:ok, user} <- autofollow_users(user), {:ok, user} <- set_cache(user), {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- try_send_confirmation_email(user) do @@ -485,6 +492,13 @@ defmodule Pleroma.User do Repo.get_by(User, ap_id: ap_id) end + def get_all_by_ap_id(ap_ids) do + from(u in __MODULE__, + where: u.ap_id in ^ap_ids + ) + |> Repo.all() + end + # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part # of the ap_id and the domain and tries to get that user def get_by_guessed_nickname(ap_id) do @@ -742,6 +756,7 @@ defmodule Pleroma.User do |> update_and_set_cache() end + @spec maybe_fetch_follow_information(User.t()) :: User.t() def maybe_fetch_follow_information(user) do with {:ok, user} <- fetch_follow_information(user) do user @@ -799,9 +814,10 @@ defmodule Pleroma.User do end end + @spec maybe_update_following_count(User.t()) :: User.t() def maybe_update_following_count(%User{local: false} = user) do if Pleroma.Config.get([:instance, :external_user_synchronization]) do - {:ok, maybe_fetch_follow_information(user)} + maybe_fetch_follow_information(user) else user end @@ -907,6 +923,13 @@ defmodule Pleroma.User do blocker end + # clear any requested follows as well + blocked = + case CommonAPI.reject_follow_request(blocked, blocker) do + {:ok, %User{} = updated_blocked} -> updated_blocked + nil -> blocked + end + blocker = if subscribed_to?(blocked, blocker) do {:ok, blocker} = unsubscribe(blocked, blocker) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 22eb9a182..779bfbc18 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -49,6 +49,8 @@ defmodule Pleroma.User.Info do field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) field(:pleroma_settings_store, :map, default: %{}) + field(:fields, {:array, :map}, default: nil) + field(:raw_fields, {:array, :map}, default: []) field(:notification_settings, :map, default: %{ @@ -254,11 +256,13 @@ defmodule Pleroma.User.Info do :hide_followers, :hide_follows, :follower_count, + :fields, :following_count ]) + |> validate_fields(true) end - def user_upgrade(info, params) do + def user_upgrade(info, params, remote? \\ false) do info |> cast(params, [ :ap_enabled, @@ -269,8 +273,10 @@ defmodule Pleroma.User.Info do :follower_count, :following_count, :hide_follows, + :fields, :hide_followers ]) + |> validate_fields(remote?) end def profile_update(info, params) do @@ -286,10 +292,40 @@ defmodule Pleroma.User.Info do :background, :show_role, :skip_thread_containment, + :fields, + :raw_fields, :pleroma_settings_store ]) + |> validate_fields() end + def validate_fields(changeset, remote? \\ false) do + limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields + limit = Pleroma.Config.get([:instance, limit_name], 0) + + changeset + |> validate_length(:fields, max: limit) + |> validate_change(:fields, fn :fields, fields -> + if Enum.all?(fields, &valid_field?/1) do + [] + else + [fields: "invalid"] + end + end) + end + + defp valid_field?(%{"name" => name, "value" => value}) do + name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) + + is_binary(name) && + is_binary(value) && + String.length(name) <= name_limit && + String.length(value) <= value_limit + end + + defp valid_field?(_), do: false + @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t() def confirmation_changeset(info, opts) do need_confirmation? = Keyword.get(opts, :need_confirmation) @@ -384,6 +420,21 @@ defmodule Pleroma.User.Info do cast(info, params, [:muted_reblogs]) end + # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. + # For example: [{"name": "Pronoun", "value": "she/her"}, …] + def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do + limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) + + attachment + |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) + |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + |> Enum.take(limit) + end + + def fields(%{fields: nil}), do: [] + + def fields(%{fields: fields}), do: fields + def follow_information_update(info, params) do info |> cast(params, [ diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 01052846f..172c952d4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -65,12 +65,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do if not is_nil(actor) do with user <- User.get_cached_by_ap_id(actor), false <- user.info.deactivated do - :ok + true else - _e -> :reject + _e -> false end else - :ok + true end end @@ -119,10 +119,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def increase_poll_votes_if_vote(_create_data), do: :noop - def insert(map, local \\ true, fake \\ false) when is_map(map) do + def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), - :ok <- check_actor_is_active(map["actor"]), + true <- bypass_actor_check || check_actor_is_active(map["actor"]), {_, true} <- {:remote_limit_error, check_remote_limit(map)}, {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), @@ -411,7 +411,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "actor" => ap_id, "object" => %{"type" => "Person", "id" => ap_id} }, - {:ok, activity} <- insert(data, true, true), + {:ok, activity} <- insert(data, true, true, true), :ok <- maybe_federate(activity) do {:ok, user} end @@ -791,14 +791,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query - defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do + defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do mutes = info.mutes - from( - activity in query, - where: fragment("not (? = ANY(?))", activity.actor, ^mutes), - where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes) - ) + query = + from([activity] in query, + where: fragment("not (? = ANY(?))", activity.actor, ^mutes), + where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes) + ) + + unless opts["skip_preload"] do + from([thread_mute: tm] in query, where: is_nil(tm)) + else + query + end end defp restrict_muted(query, _), do: query @@ -899,7 +905,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_set_thread_muted_field(query, opts) do query - |> Activity.with_set_thread_muted_field(opts["user"]) + |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"]) end defp maybe_order(query, %{order: :desc}) do @@ -1017,6 +1023,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "url" => [%{"href" => data["image"]["url"]}] } + fields = + data + |> Map.get("attachment", []) + |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) + |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + locked = data["manuallyApprovesFollowers"] || false data = Transmogrifier.maybe_fix_user_object(data) @@ -1026,6 +1038,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ap_enabled: true, source_data: data, banner: banner, + fields: fields, locked: locked }, avatar: avatar, diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 133a726c5..ed801a7ae 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) else nil -> {:error, :not_found} @@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %Object{} = object <- Object.get_cached_by_ap_id(ap_id), {_, true} <- {:public?, Visibility.is_public?(object)} do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("object.json", %{object: object})) else {:public?, false} -> @@ -69,7 +69,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("likes.json", ap_id, likes, page)) else {:public?, false} -> @@ -83,7 +83,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {_, true} <- {:public?, Visibility.is_public?(object)}, likes <- Utils.get_object_likes(object) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("likes.json", ap_id, likes)) else {:public?, false} -> @@ -96,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do %Activity{} = activity <- Activity.normalize(ap_id), {_, true} <- {:public?, Visibility.is_public?(activity)} do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(ObjectView.render("object.json", %{object: activity})) else {:public?, false} -> @@ -104,6 +104,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + # GET /relay/following + def following(%{assigns: %{relay: true}} = conn, _params) do + conn + |> put_resp_content_type("application/activity+json") + |> json(UserView.render("following.json", %{user: Relay.get_actor()})) + end + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), @@ -112,12 +119,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) else {:show_follows, _} -> conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> send_resp(403, "") end end @@ -126,11 +133,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("following.json", %{user: user, for: for_user})) end end + # GET /relay/followers + def followers(%{assigns: %{relay: true}} = conn, _params) do + conn + |> put_resp_content_type("application/activity+json") + |> json(UserView.render("followers.json", %{user: Relay.get_actor()})) + end + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), @@ -139,12 +153,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {page, _} = Integer.parse(page) conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) else {:show_followers, _} -> conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> send_resp(403, "") end end @@ -153,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("followers.json", %{user: user, for: for_user})) end end @@ -162,7 +176,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) end end @@ -210,7 +224,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do defp represent_service_actor(%User{} = user, conn) do with {:ok, user} <- User.ensure_keys_present(user) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) else nil -> {:error, :not_found} @@ -231,7 +245,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("user.json", %{user: user})) end @@ -240,7 +254,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do if nickname == user.nickname do conn - |> put_resp_header("content-type", "application/activity+json") + |> put_resp_content_type("application/activity+json") |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) else err = diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 262529b84..c97405690 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -50,9 +50,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + date = Pleroma.Signature.signed_date() signature = Pleroma.Signature.sign(actor, %{ diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 5f18cc64a..c2ac38907 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -22,13 +22,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} else - {:error, _} = error -> - Logger.error("error: #{inspect(error)}") - error - - e -> - Logger.error("error: #{inspect(e)}") - {:error, e} + error -> format_error(error) end end @@ -37,16 +31,11 @@ defmodule Pleroma.Web.ActivityPub.Relay do with %User{} = local_user <- get_actor(), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do + User.unfollow(local_user, target_user) Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} else - {:error, _} = error -> - Logger.error("error: #{inspect(error)}") - error - - e -> - Logger.error("error: #{inspect(e)}") - {:error, e} + error -> format_error(error) end end @@ -56,11 +45,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do %Object{} = object <- Object.normalize(activity) do ActivityPub.announce(user, object, nil, true, false) else - e -> - Logger.error("error: #{inspect(e)}") - {:error, inspect(e)} + error -> format_error(error) end end def publish(_), do: {:error, "Not implemented"} + + defp format_error({:error, error}), do: format_error(error) + + defp format_error(error) do + Logger.error("error: #{inspect(error)}") + {:error, error} + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 0fcc81bf3..36340a3a1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -598,14 +598,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do banner = new_user_data[:info][:banner] locked = new_user_data[:info][:locked] || false + attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || [] + + fields = + attachment + |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) + |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) update_data = new_user_data |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, %{banner: banner, locked: locked}) + |> Map.put(:info, %{banner: banner, locked: locked, fields: fields}) actor - |> User.upgrade_changeset(update_data) + |> User.upgrade_changeset(update_data, true) |> User.update_and_set_cache() ActivityPub.update(%{ diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 06c9e1c71..7be734b26 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -80,6 +80,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Transmogrifier.add_emoji_tags() |> Map.get("tag", []) + fields = + user.info + |> User.Info.fields() + |> Enum.map(fn %{"name" => name, "value" => value} -> + %{ + "name" => Pleroma.HTML.strip_tags(name), + "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) + } + end) + |> Enum.map(&Map.put(&1, "type", "PropertyValue")) + %{ "id" => user.ap_id, "type" => "Person", @@ -98,6 +109,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do "publicKeyPem" => public_key }, "endpoints" => endpoints, + "attachment" => fields, "tag" => (user.info.source_data["tag"] || []) ++ user_tags } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2d3d0adc4..544b9d7d8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller alias Pleroma.Activity + alias Pleroma.ModerationLog alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub @@ -12,6 +13,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.ConfigView + alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -25,52 +27,113 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do action_fallback(:errors) - def user_delete(conn, %{"nickname" => nickname}) do - User.get_cached_by_nickname(nickname) - |> User.delete() + def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + user = User.get_cached_by_nickname(nickname) + User.delete(user) + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: "delete" + }) conn |> json(nickname) end - def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_follow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.follow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "follow" + }) end conn |> json("ok") end - def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do + def user_unfollow(%{assigns: %{user: admin}} = conn, %{ + "follower" => follower_nick, + "followed" => followed_nick + }) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.unfollow(follower, followed) + + ModerationLog.insert_log(%{ + actor: admin, + followed: followed, + follower: follower, + action: "unfollow" + }) end conn |> json("ok") end - def user_create( - conn, - %{"nickname" => nickname, "email" => email, "password" => password} - ) do - user_data = %{ - nickname: nickname, - name: nickname, - email: email, - password: password, - password_confirmation: password, - bio: "." - } + def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do + changesets = + Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> + user_data = %{ + nickname: nickname, + name: nickname, + email: email, + password: password, + password_confirmation: password, + bio: "." + } - changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) - {:ok, user} = User.register(changeset) + User.register_changeset(%User{}, user_data, need_confirmation: false) + end) + |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi -> + Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset) + end) - conn - |> json(user.nickname) + case Pleroma.Repo.transaction(changesets) do + {:ok, users} -> + res = + users + |> Map.values() + |> Enum.map(fn user -> + {:ok, user} = User.post_register_action(user) + + user + end) + |> Enum.map(&AccountView.render("created.json", %{user: &1})) + + ModerationLog.insert_log(%{ + actor: admin, + subjects: Map.values(users), + action: "create" + }) + + conn + |> json(res) + + {:error, id, changeset, _} -> + res = + Enum.map(changesets.operations, fn + {current_id, {:changeset, _current_changeset, _}} when current_id == id -> + AccountView.render("create-error.json", %{changeset: changeset}) + + {_, {:changeset, current_changeset, _}} -> + AccountView.render("create-error.json", %{changeset: current_changeset}) + end) + + conn + |> put_status(:conflict) + |> json(res) + end end def user_show(conn, %{"nickname" => nickname}) do @@ -101,23 +164,47 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def user_toggle_activation(conn, %{"nickname" => nickname}) do + def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) {:ok, updated_user} = User.deactivate(user, !user.info.deactivated) + action = if user.info.deactivated, do: "activate", else: "deactivate" + + ModerationLog.insert_log(%{ + actor: admin, + subject: user, + action: action + }) + conn |> json(AccountView.render("show.json", %{user: updated_user})) end - def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.tag(nicknames, tags), - do: json_response(conn, :no_content, "") + def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.tag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "tag" + }) + + json_response(conn, :no_content, "") + end end - def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do - with {:ok, _} <- User.untag(nicknames, tags), - do: json_response(conn, :no_content, "") + def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do + with {:ok, _} <- User.untag(nicknames, tags) do + ModerationLog.insert_log(%{ + actor: admin, + nicknames: nicknames, + tags: tags, + action: "untag" + }) + + json_response(conn, :no_content, "") + end end def list_users(conn, params) do @@ -158,7 +245,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Enum.into(%{}, &{&1, true}) end - def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) + def right_add(%{assigns: %{user: admin}} = conn, %{ + "permission_group" => permission_group, + "nickname" => nickname + }) when permission_group in ["moderator", "admin"] do user = User.get_cached_by_nickname(nickname) @@ -173,6 +263,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do |> Ecto.Changeset.change() |> Ecto.Changeset.put_embed(:info, info_cng) + ModerationLog.insert_log(%{ + action: "grant", + actor: admin, + subject: user, + permission: permission_group + }) + {:ok, _user} = User.update_and_set_cache(cng) json(conn, info) @@ -193,7 +290,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_delete( - %{assigns: %{user: %User{:nickname => admin_nickname}}} = conn, + %{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname @@ -217,6 +314,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, _user} = User.update_and_set_cache(cng) + ModerationLog.insert_log(%{ + action: "revoke", + actor: admin, + subject: user, + permission: permission_group + }) + json(conn, info) end end @@ -225,15 +329,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do render_error(conn, :not_found, "No such permission_group") end - def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do + 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: json_response(conn, :no_content, "") + {: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, "") + end end - def relay_follow(conn, %{"relay_url" => target}) do + def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.follow(target) do + ModerationLog.insert_log(%{ + action: "relay_follow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -243,8 +365,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def relay_unfollow(conn, %{"relay_url" => target}) do + def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do with {:ok, _message} <- Relay.unfollow(target) do + ModerationLog.insert_log(%{ + action: "relay_unfollow", + actor: admin, + target: target + }) + json(conn, target) else _ -> @@ -335,8 +463,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def report_update_state(conn, %{"id" => id, "state" => state}) do + def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do with {:ok, report} <- CommonAPI.update_report_state(id, state) do + ModerationLog.insert_log(%{ + action: "report_update", + actor: admin, + subject: report + }) + conn |> put_view(ReportView) |> render("show.json", %{report: report}) @@ -353,6 +487,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do {:ok, activity} = CommonAPI.post(user, params) + ModerationLog.insert_log(%{ + action: "report_response", + actor: user, + subject: activity, + text: params["status"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -365,8 +506,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end - def status_update(conn, %{"id" => id} = params) do + def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do + {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"]) + + ModerationLog.insert_log(%{ + action: "status_update", + actor: admin, + subject: activity, + sensitive: sensitive, + visibility: params["visibility"] + }) + conn |> put_view(StatusView) |> render("status.json", %{activity: activity}) @@ -375,10 +526,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + ModerationLog.insert_log(%{ + action: "status_delete", + actor: user, + subject_id: id + }) + json(conn, %{}) end end + def list_log(conn, params) do + {page, page_size} = page_params(params) + + log = ModerationLog.get_all(page, page_size) + + conn + |> put_view(ModerationLogView) + |> render("index.json", %{log: log}) + end + def migrate_to_db(conn, _params) do Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) json(conn, %{}) diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 7e1b9c431..a96affd40 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -52,4 +52,50 @@ defmodule Pleroma.Web.AdminAPI.AccountView do invites: render_many(invites, AccountView, "invite.json", as: :invite) } end + + def render("created.json", %{user: user}) do + %{ + type: "success", + code: 200, + data: %{ + nickname: user.nickname, + email: user.email + } + } + end + + def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do + %{ + type: "error", + code: 409, + error: parse_error(errors), + data: %{ + nickname: Map.get(changes, :nickname), + email: Map.get(changes, :email) + } + } + end + + defp parse_error([]), do: "" + + defp parse_error(errors) do + ## when nickname is duplicate ap_id constraint error is raised + nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id) + email_error = Keyword.get(errors, :email) + password_error = Keyword.get(errors, :password) + + cond do + nickname_error -> + "nickname #{elem(nickname_error, 0)}" + + email_error -> + "email #{elem(email_error, 0)}" + + password_error -> + "password #{elem(password_error, 0)}" + + true -> + "" + end + end end diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex new file mode 100644 index 000000000..b3fc7cfe5 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ModerationLogView do + use Pleroma.Web, :view + + alias Pleroma.ModerationLog + + def render("index.json", %{log: log}) do + render_many(log, __MODULE__, "show.json", as: :log_entry) + end + + def render("show.json", %{log_entry: log_entry}) do + time = + log_entry.inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + + %{ + data: log_entry.data, + time: time, + message: ModerationLog.get_log_entry_message(log_entry) + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2db58324b..5faddc9f4 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute @@ -171,21 +173,25 @@ defmodule Pleroma.Web.CommonAPI do end) end - def get_visibility(%{"visibility" => visibility}, in_reply_to) + def get_visibility(_, _, %Participation{}) do + {"direct", "direct"} + end + + def get_visibility(%{"visibility" => visibility}, in_reply_to, _) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} - def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do + def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do visibility = {:list, String.to_integer(list_id)} {visibility, get_replied_to_visibility(in_reply_to)} end - def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do + def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do visibility = get_replied_to_visibility(in_reply_to) {visibility, visibility} end - def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)} + def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)} def get_replied_to_visibility(nil), do: nil @@ -195,13 +201,32 @@ defmodule Pleroma.Web.CommonAPI do end end + defp check_expiry_date({:ok, nil} = res), do: res + + defp check_expiry_date({:ok, in_seconds}) do + expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds) + + if ActivityExpiration.expires_late_enough?(expiry) do + {:ok, expiry} + else + {:error, "Expiry date is too soon"} + end + end + + defp check_expiry_date(expiry_str) do + Ecto.Type.cast(:integer, expiry_str) + |> check_expiry_date() + end + def post(user, %{"status" => status} = data) do limit = Pleroma.Config.get([:instance, :limit]) with status <- String.trim(status), attachments <- attachments_from_ids(data), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), - {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to), + in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]), + {visibility, in_reply_to_visibility} <- + get_visibility(data, in_reply_to, in_reply_to_conversation), {_, false} <- {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, {content_html, mentions, tags} <- @@ -214,10 +239,12 @@ defmodule Pleroma.Web.CommonAPI do mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), addressed_users <- get_addressed_users(mentioned_users, data["to"]), {poll, poll_emoji} <- make_poll_data(data), - {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility), - context <- make_context(in_reply_to), + {to, cc} <- + get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation), + context <- make_context(in_reply_to, in_reply_to_conversation), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), + {:ok, expires_at} <- check_expiry_date(data["expires_in"]), full_payload <- String.trim(status <> cw), :ok <- validate_character_limit(full_payload, attachments, limit), object <- @@ -243,15 +270,24 @@ defmodule Pleroma.Web.CommonAPI do preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false direct? = visibility == "direct" - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => direct?} - } - |> maybe_add_list_data(user, visibility) - |> ActivityPub.create(preview?) + result = + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) + + if expires_at do + with {:ok, activity} <- result do + {:ok, _} = ActivityExpiration.create(activity, expires_at) + end + end + + result else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 22c44a0a3..6958c7511 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Calendar.Strftime alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.Plugs.AuthenticationPlug @@ -86,9 +87,20 @@ defmodule Pleroma.Web.CommonAPI.Utils do |> Enum.filter(& &1) end - @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: - {list(String.t()), list(String.t())} - def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do + @spec get_to_and_cc( + User.t(), + list(String.t()), + Activity.t() | nil, + String.t(), + Participation.t() | nil + ) :: {list(String.t()), list(String.t())} + + def get_to_and_cc(_, _, _, _, %Participation{} = participation) do + participation = Repo.preload(participation, :recipients) + {Enum.map(participation.recipients, & &1.ap_id), []} + end + + def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do to = [Pleroma.Constants.as_public() | mentioned_users] cc = [user.follower_address] @@ -99,7 +111,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do + def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do to = [user.follower_address | mentioned_users] cc = [Pleroma.Constants.as_public()] @@ -110,12 +122,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do - {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct") + def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do + {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil) {[user.follower_address | to], cc} end - def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do + def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do if inReplyTo do {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []} else @@ -123,7 +135,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []} def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) @@ -253,8 +265,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do defp maybe_add_nsfw_tag(data, _), do: data - def make_context(%Activity{data: %{"context" => context}}), do: context - def make_context(_), do: Utils.generate_context_id() + def make_context(_, %Participation{} = participation) do + Repo.preload(participation, :conversation).conversation.ap_id + end + + def make_context(%Activity{data: %{"context" => context}}, _), do: context + def make_context(_, _), do: Utils.generate_context_id() def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 8a753bb4f..eeac9f503 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -33,4 +33,80 @@ defmodule Pleroma.Web.ControllerHelper do end defp param_to_integer(_, default), do: default + + def add_link_headers( + conn, + method, + activities, + param \\ nil, + params \\ %{}, + func3 \\ nil, + func4 \\ nil + ) do + params = + conn.params + |> Map.drop(["since_id", "max_id", "min_id"]) + |> Map.merge(params) + + last = List.last(activities) + + func3 = func3 || (&mastodon_api_url/3) + func4 = func4 || (&mastodon_api_url/4) + + if last do + max_id = last.id + + limit = + params + |> Map.get("limit", "20") + |> String.to_integer() + + min_id = + if length(activities) <= limit do + activities + |> List.first() + |> Map.get(:id) + else + activities + |> Enum.at(limit * -1) + |> Map.get(:id) + end + + {next_url, prev_url} = + if param do + { + func4.( + Pleroma.Web.Endpoint, + method, + param, + Map.merge(params, %{max_id: max_id}) + ), + func4.( + Pleroma.Web.Endpoint, + method, + param, + Map.merge(params, %{min_id: min_id}) + ) + } + else + { + func3.( + Pleroma.Web.Endpoint, + method, + Map.merge(params, %{max_id: max_id}) + ), + func3.( + Pleroma.Web.Endpoint, + method, + Map.merge(params, %{min_id: min_id}) + ) + } + end + + conn + |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + else + conn + end + end end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 7ce2b5b06..53cf95fbb 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -5,7 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [json_response: 3] + import Pleroma.Web.ControllerHelper, + only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] alias Ecto.Changeset alias Pleroma.Activity @@ -137,7 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") user_info_emojis = - ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) + user.info + |> Map.get(:emoji, []) + |> Enum.concat(Formatter.get_emoji_map(emojis_text)) |> Enum.dedup() info_params = @@ -156,6 +159,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end) end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "fields", :fields, fn fields -> + fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) + + {:ok, fields} + end) + |> add_if_present(params, "fields", :raw_fields) |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> {:ok, Map.merge(user.info.pleroma_settings_store, value)} end) @@ -342,71 +351,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, mastodon_emoji) end - defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do - params = - conn.params - |> Map.drop(["since_id", "max_id", "min_id"]) - |> Map.merge(params) - - last = List.last(activities) - - if last do - max_id = last.id - - limit = - params - |> Map.get("limit", "20") - |> String.to_integer() - - min_id = - if length(activities) <= limit do - activities - |> List.first() - |> Map.get(:id) - else - activities - |> Enum.at(limit * -1) - |> Map.get(:id) - end - - {next_url, prev_url} = - if param do - { - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - param, - Map.merge(params, %{max_id: max_id}) - ), - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - param, - Map.merge(params, %{min_id: min_id}) - ) - } - else - { - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - Map.merge(params, %{max_id: max_id}) - ), - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - Map.merge(params, %{min_id: min_id}) - ) - } - end - - conn - |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") - else - conn - end - end - def home_timeline(%{assigns: %{user: user}} = conn, params) do params = params @@ -1797,7 +1741,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conversations = Enum.map(participations, fn participation -> - ConversationView.render("participation.json", %{participation: participation, user: user}) + ConversationView.render("participation.json", %{participation: participation, for: user}) end) conn @@ -1810,7 +1754,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do Repo.get_by(Participation, id: participation_id, user_id: user.id), {:ok, participation} <- Participation.mark_as_read(participation) do participation_view = - ConversationView.render("participation.json", %{participation: participation, user: user}) + ConversationView.render("participation.json", %{participation: participation, for: user}) conn |> json(participation_view) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 0ef568f0f..169116d0d 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -94,12 +94,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end) fields = - (user.info.source_data["attachment"] || []) - |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) - |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + user.info + |> User.Info.fields() + |> Enum.map(fn %{"name" => name, "value" => value} -> + %{ + "name" => Pleroma.HTML.strip_tags(name), + "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) + } + end) + + raw_fields = Map.get(user.info, :raw_fields, []) bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for])) - relationship = render("relationship.json", %{user: opts[:for], target: user}) %{ @@ -124,6 +130,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do source: %{ note: HTML.strip_tags((user.bio || "") |> String.replace("
", "\n")), sensitive: false, + fields: raw_fields, pleroma: %{} }, diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 38bdec737..40acc07b3 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -11,8 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView - def render("participation.json", %{participation: participation, user: user}) do - participation = Repo.preload(participation, conversation: :users) + def render("participation.json", %{participation: participation, for: user}) do + participation = Repo.preload(participation, conversation: [], recipients: []) last_activity_id = with nil <- participation.last_activity_id do @@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do # Conversations return all users except the current user. users = - participation.conversation.users + participation.recipients |> Enum.reject(&(&1.id == user.id)) accounts = diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7e4e99280..a4ee0b5dd 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do require Pleroma.Constants alias Pleroma.Activity + alias Pleroma.ActivityExpiration + alias Pleroma.Conversation + alias Pleroma.Conversation.Participation alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -175,6 +178,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil + client_posted_this_activity = opts[:for] && user.id == opts[:for].id + + expires_at = + with true <- client_posted_this_activity, + expiration when not is_nil(expiration) <- + ActivityExpiration.get_by_activity_id(activity.id) do + expiration.scheduled_at + end + thread_muted? = case activity.thread_muted? do thread_muted? when is_boolean(thread_muted?) -> thread_muted? @@ -235,6 +247,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do object.data["url"] || object.data["external_url"] || object.data["id"] end + direct_conversation_id = + with {_, true} <- {:include_id, opts[:with_direct_conversation_id]}, + {_, %User{} = for_user} <- {:for_user, opts[:for]}, + %{data: %{"context" => context}} when is_binary(context) <- activity, + %Conversation{} = conversation <- Conversation.get_for_ap_id(context), + %Participation{id: participation_id} <- + Participation.for_user_and_conversation(for_user, conversation) do + participation_id + else + _e -> + nil + end + %{ id: to_string(activity.id), uri: object.data["id"], @@ -272,7 +297,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do conversation_id: get_context_id(activity), in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, - spoiler_text: %{"text/plain" => summary_plaintext} + spoiler_text: %{"text/plain" => summary_plaintext}, + expires_at: expires_at, + direct_conversation_id: direct_conversation_id } } end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index c70063b84..07e2a4c2d 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -22,6 +22,11 @@ defmodule Pleroma.Web.OStatus.OStatusController do 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( @@ -32,8 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do action_fallback(:errors) def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do - with {_, %User{} = user} <- - {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do + with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do RedirectController.redirector_with_meta(conn, %{user: user}) end end diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex new file mode 100644 index 000000000..b6d2bf86b --- /dev/null +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7] + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.ConversationView + alias Pleroma.Web.MastodonAPI.StatusView + + def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- Participation.get(participation_id), + true <- user.id == participation.user_id do + conn + |> put_view(ConversationView) + |> render("participation.json", %{participation: participation, for: user}) + end + end + + def conversation_statuses( + %{assigns: %{user: user}} = conn, + %{"id" => participation_id} = params + ) do + params = + params + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + participation = + participation_id + |> Participation.get(preload: [:conversation]) + + if user.id == participation.user_id do + activities = + participation.conversation.ap_id + |> ActivityPub.fetch_activities_for_context(params) + |> Enum.reverse() + + conn + |> add_link_headers( + :conversation_statuses, + activities, + participation_id, + params, + nil, + &pleroma_api_url/4 + ) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + end + + def update_conversation( + %{assigns: %{user: user}} = conn, + %{"id" => participation_id, "recipients" => recipients} + ) do + participation = + participation_id + |> Participation.get() + + with true <- user.id == participation.user_id, + {:ok, participation} <- Participation.set_recipients(participation, recipients) do + conn + |> put_view(ConversationView) + |> render("participation.json", %{participation: participation, for: user}) + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c8c1c22dd..1ad33630c 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -133,6 +133,10 @@ defmodule Pleroma.Web.Router do }) end + pipeline :http_signature do + plug(Pleroma.Web.Plugs.HTTPSignaturePlug) + end + scope "/api/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:pleroma_api) @@ -155,7 +159,7 @@ defmodule Pleroma.Web.Router do post("/users/unfollow", AdminAPIController, :user_unfollow) delete("/users", AdminAPIController, :user_delete) - post("/users", AdminAPIController, :user_create) + post("/users", AdminAPIController, :users_create) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) @@ -198,6 +202,8 @@ defmodule Pleroma.Web.Router do post("/config", AdminAPIController, :config_update) get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) + + get("/moderation_log", AdminAPIController, :list_log) end scope "/", Pleroma.Web.TwitterAPI do @@ -259,6 +265,21 @@ defmodule Pleroma.Web.Router do end end + scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do + pipe_through(:authenticated_api) + + scope [] do + pipe_through(:oauth_read) + get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) + get("/conversations/:id", PleromaAPIController, :conversation) + end + + scope [] do + pipe_through(:oauth_write) + patch("/conversations/:id", PleromaAPIController, :update_conversation) + end + end + scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) @@ -671,7 +692,14 @@ defmodule Pleroma.Web.Router do pipe_through(:ap_service_actor) get("/", ActivityPubController, :relay) - post("/inbox", ActivityPubController, :inbox) + + scope [] do + pipe_through(:http_signature) + post("/inbox", ActivityPubController, :inbox) + end + + get("/following", ActivityPubController, :following, assigns: %{relay: true}) + get("/followers", ActivityPubController, :followers, assigns: %{relay: true}) end scope "/internal/fetch", Pleroma.Web.ActivityPub do diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index bbaddd852..587c43f40 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -113,8 +113,7 @@ defmodule Pleroma.Web.Streamer do |> Map.get("#{topic}:#{item.user_id}", []) |> Enum.each(fn socket -> with %User{} = user <- User.get_cached_by_ap_id(socket.assigns[:user].ap_id), - true <- should_send?(user, item), - false <- CommonAPI.thread_muted?(user, item.activity) do + true <- should_send?(user, item) do send( socket.transport_pid, {:text, represent_notification(socket.assigns[:user], item)} @@ -202,7 +201,7 @@ defmodule Pleroma.Web.Streamer do payload: Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ participation: participation, - user: participation.user + for: participation.user }) |> Jason.encode!() } @@ -236,7 +235,8 @@ defmodule Pleroma.Web.Streamer do %{host: parent_host} <- URI.parse(parent.data["actor"]), false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), - true <- thread_containment(item, user) do + true <- thread_containment(item, user), + false <- CommonAPI.thread_muted?(user, item) do true else _ -> false diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex index c9dd699fd..860df5f9c 100644 --- a/lib/pleroma/web/templates/email/digest.html.eex +++ b/lib/pleroma/web/templates/email/digest.html.eex @@ -1,20 +1,568 @@ -

Hey <%= @user.nickname %>, here is what you've missed!

+ -

New Mentions:

-
    -<%= for %{data: mention, object: object, from: from} <- @mentions do %> -
  • <%= link from.nickname, to: mention.activity.actor %>: <%= raw object.data["content"] %>
  • -<% end %> -
+ -<%= if @followers != [] do %> -

<%= length(@followers) %> New Followers:

-
    -<%= for %{data: follow, from: from} <- @followers do %> -
  • <%= link from.nickname, to: follow.activity.actor %>
  • -<% end %> -
-<% end %> + + + + + + + + <%= @email.subject %>< + + + + + + + + + + + + + + + + + + + diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index 8d8892068..8a7d2fc72 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -74,12 +74,15 @@ defmodule Pleroma.Web.TwitterAPI.UserView do |> HTML.filter_tags(User.html_filter_policy(for_user)) |> Formatter.emojify(emoji) - # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. - # For example: [{"name": "Pronoun", "value": "she/her"}, …] fields = - (user.info.source_data["attachment"] || []) - |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) - |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + user.info + |> User.Info.fields() + |> Enum.map(fn %{"name" => name, "value" => value} -> + %{ + "name" => Pleroma.HTML.strip_tags(name), + "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) + } + end) data = %{ diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex index b63eb162c..b506a234b 100644 --- a/lib/pleroma/web/views/email_view.ex +++ b/lib/pleroma/web/views/email_view.ex @@ -2,4 +2,14 @@ defmodule Pleroma.Web.EmailView do use Pleroma.Web, :view import Phoenix.HTML import Phoenix.HTML.Link + + def avatar_url(user) do + Pleroma.User.avatar_url(user) + end + + def format_date(date) when is_binary(date) do + date + |> Timex.parse!("{ISO:Extended:Z}") + |> Timex.format!("{Mshort} {D}, {YYYY} {h24}:{m}") + end end diff --git a/priv/repo/migrations/20190205114625_create_thread_mutes.exs b/priv/repo/migrations/20190205114625_create_thread_mutes.exs index 7e44db121..baaf07253 100644 --- a/priv/repo/migrations/20190205114625_create_thread_mutes.exs +++ b/priv/repo/migrations/20190205114625_create_thread_mutes.exs @@ -6,7 +6,7 @@ defmodule Pleroma.Repo.Migrations.CreateThreadMutes do add :user_id, references(:users, type: :uuid, on_delete: :delete_all) add :context, :string end - + create_if_not_exists unique_index(:thread_mutes, [:user_id, :context], name: :unique_index) end end diff --git a/priv/repo/migrations/20190716100804_add_expirations_table.exs b/priv/repo/migrations/20190716100804_add_expirations_table.exs new file mode 100644 index 000000000..fbde8f9d6 --- /dev/null +++ b/priv/repo/migrations/20190716100804_add_expirations_table.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddExpirationsTable do + use Ecto.Migration + + def change do + create_if_not_exists table(:activity_expirations) do + add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all)) + add(:scheduled_at, :naive_datetime, null: false) + end + end +end diff --git a/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs new file mode 100644 index 000000000..c6e3469d5 --- /dev/null +++ b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.CreateConversationParticipationRecipientShips do + use Ecto.Migration + + def change do + create_if_not_exists table(:conversation_participation_recipient_ships) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:participation_id, references(:conversation_participations, on_delete: :delete_all)) + end + + create_if_not_exists index(:conversation_participation_recipient_ships, [:user_id]) + create_if_not_exists index(:conversation_participation_recipient_ships, [:participation_id]) + end +end diff --git a/priv/repo/migrations/20190818124341_create_moderation_log.exs b/priv/repo/migrations/20190818124341_create_moderation_log.exs new file mode 100644 index 000000000..cef6636f3 --- /dev/null +++ b/priv/repo/migrations/20190818124341_create_moderation_log.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Repo.Migrations.CreateModerationLog do + use Ecto.Migration + + def change do + create table(:moderation_log) do + add(:data, :map) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20190823000549_add_likes_index_to_objects.exs b/priv/repo/migrations/20190823000549_add_likes_index_to_objects.exs new file mode 100644 index 000000000..13f3d6e83 --- /dev/null +++ b/priv/repo/migrations/20190823000549_add_likes_index_to_objects.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddLikesIndexToObjects do + use Ecto.Migration + + def change do + create_if_not_exists index(:objects, ["(data->'likes')"], using: :gin, name: :objects_likes) + end +end diff --git a/test/activity_expiration_test.exs b/test/activity_expiration_test.exs new file mode 100644 index 000000000..4948fae16 --- /dev/null +++ b/test/activity_expiration_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationTest do + use Pleroma.DataCase + alias Pleroma.ActivityExpiration + import Pleroma.Factory + + test "finds activities due to be deleted only" do + activity = insert(:note_activity) + expiration_due = insert(:expiration_in_the_past, %{activity_id: activity.id}) + activity2 = insert(:note_activity) + insert(:expiration_in_the_future, %{activity_id: activity2.id}) + + expirations = ActivityExpiration.due_expirations() + + assert length(expirations) == 1 + assert hd(expirations) == expiration_due + end + + test "denies expirations that don't live long enough" do + activity = insert(:note_activity) + now = NaiveDateTime.utc_now() + assert {:error, _} = ActivityExpiration.create(activity, now) + end +end diff --git a/test/activity_expiration_worker_test.exs b/test/activity_expiration_worker_test.exs new file mode 100644 index 000000000..939d912f1 --- /dev/null +++ b/test/activity_expiration_worker_test.exs @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ActivityExpirationWorkerTest do + use Pleroma.DataCase + alias Pleroma.Activity + import Pleroma.Factory + + test "deletes an activity" do + activity = insert(:note_activity) + expiration = insert(:expiration_in_the_past, %{activity_id: activity.id}) + Pleroma.ActivityExpirationWorker.perform(:execute, expiration.id) + + refute Repo.get(Activity, activity.id) + end +end diff --git a/test/activity_test.exs b/test/activity_test.exs index b27f6fd36..785c4b3cf 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -164,4 +164,13 @@ defmodule Pleroma.ActivityTest do Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) end end + + test "add an activity with an expiration" do + activity = insert(:note_activity) + insert(:expiration_in_the_future, %{activity_id: activity.id}) + + Pleroma.ActivityExpiration + |> where([a], a.activity_id == ^activity.id) + |> Repo.one!() + end end diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs index 4455a4d47..9074f3b97 100644 --- a/test/config/transfer_task_test.exs +++ b/test/config/transfer_task_test.exs @@ -5,14 +5,8 @@ defmodule Pleroma.Config.TransferTaskTest do use Pleroma.DataCase - setup do - dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) - + clear_config([:instance, :dynamic_configuration]) do Pleroma.Config.put([:instance, :dynamic_configuration], true) - - on_exit(fn -> - Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) - end) end test "transfer config values from db to env" do diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index 2a03e5d67..a27167d42 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -8,6 +8,50 @@ defmodule Pleroma.Conversation.ParticipationTest do alias Pleroma.Conversation.Participation alias Pleroma.Web.CommonAPI + test "getting a participation will also preload things" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + + participation = Participation.get(participation.id, preload: [:conversation]) + + assert %Pleroma.Conversation{} = participation.conversation + end + + test "for a new conversation, it sets the recipents of the participation" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + assert user in participation.recipients + assert other_user in participation.recipients + + # Mentioning another user in the same conversation will not add a new recipients. + + {:ok, _activity} = + CommonAPI.post(user, %{ + "in_reply_to_status_id" => activity.id, + "status" => "Hey @#{third_user.nickname}.", + "visibility" => "direct" + }) + + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + end + test "it creates a participation for a conversation and a user" do user = insert(:user) conversation = insert(:conversation) @@ -102,4 +146,23 @@ defmodule Pleroma.Conversation.ParticipationTest do [] = Participation.for_user_with_last_activity_id(user) end + + test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + [participation] = Participation.for_user_with_last_activity_id(user) + + participation = Repo.preload(participation, :recipients) + + assert participation.recipients |> length() == 1 + assert user in participation.recipients + + {:ok, participation} = Participation.set_recipients(participation, [other_user.id]) + + assert participation.recipients |> length() == 2 + assert user in participation.recipients + assert other_user in participation.recipients + end end diff --git a/test/conversation_test.exs b/test/conversation_test.exs index aa193e0d4..4e36494f8 100644 --- a/test/conversation_test.exs +++ b/test/conversation_test.exs @@ -11,14 +11,8 @@ defmodule Pleroma.ConversationTest do import Pleroma.Factory - setup_all do - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - - :ok + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) end test "it goes through old direct conversations" do diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs index 450bb09c7..ae5effb7a 100644 --- a/test/emails/mailer_test.exs +++ b/test/emails/mailer_test.exs @@ -15,11 +15,7 @@ defmodule Pleroma.Emails.MailerTest do to: [{"Test User", "user1@example.com"}] } - setup do - value = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) - on_exit(fn -> Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], value) end) - :ok - end + clear_config([Pleroma.Emails.Mailer, :enabled]) test "not send email when mailer is disabled" do Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) diff --git a/test/fixtures/mastodon-update.json b/test/fixtures/mastodon-update.json index f6713fea5..dbf8b6dff 100644 --- a/test/fixtures/mastodon-update.json +++ b/test/fixtures/mastodon-update.json @@ -1,10 +1,10 @@ -{ - "type": "Update", - "object": { - "url": "http://mastodon.example.org/@gargron", - "type": "Person", - "summary": "

Some bio

", - "publicKey": { +{ + "type": "Update", + "object": { + "url": "http://mastodon.example.org/@gargron", + "type": "Person", + "summary": "

Some bio

", + "publicKey": { "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n", "owner": "http://mastodon.example.org/users/gargron", "id": "http://mastodon.example.org/users/gargron#main-key" @@ -20,7 +20,27 @@ "endpoints": { "sharedInbox": "http://mastodon.example.org/inbox" }, - "icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"} + "attachment": [{ + "type": "PropertyValue", + "name": "foo", + "value": "updated" + }, + { + "type": "PropertyValue", + "name": "foo1", + "value": "updated" + } + ], + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + }, + "image": { + "type": "Image", + "mediaType": "image/png", + "url": "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } }, "id": "http://mastodon.example.org/users/gargron#updates/1519563538", "actor": "http://mastodon.example.org/users/gargron", diff --git a/test/fixtures/tesla_mock/admin@mastdon.example.org.json b/test/fixtures/tesla_mock/admin@mastdon.example.org.json index c297e4349..8159dc20a 100644 --- a/test/fixtures/tesla_mock/admin@mastdon.example.org.json +++ b/test/fixtures/tesla_mock/admin@mastdon.example.org.json @@ -1 +1,54 @@ -{"@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":"Person","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"},"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"}} +{ + "@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": "Person", + "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" + } +} diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs index 7febe84c5..170ca916f 100644 --- a/test/http/request_builder_test.exs +++ b/test/http/request_builder_test.exs @@ -4,21 +4,19 @@ defmodule Pleroma.HTTP.RequestBuilderTest do use ExUnit.Case, async: true + use Pleroma.Tests.Helpers alias Pleroma.HTTP.RequestBuilder describe "headers/2" do + clear_config([:http, :send_user_agent]) + test "don't send pleroma user agent" do assert RequestBuilder.headers(%{}, []) == %{headers: []} end test "send pleroma user agent" do - send = Pleroma.Config.get([:http, :send_user_agent]) Pleroma.Config.put([:http, :send_user_agent], true) - on_exit(fn -> - Pleroma.Config.put([:http, :send_user_agent], send) - end) - assert RequestBuilder.headers(%{}, []) == %{ headers: [{"User-Agent", Pleroma.Application.user_agent()}] } diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs new file mode 100644 index 000000000..c78708471 --- /dev/null +++ b/test/moderation_log_test.exs @@ -0,0 +1,301 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ModerationLogTest do + alias Pleroma.Activity + alias Pleroma.ModerationLog + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "user moderation" do + setup do + admin = insert(:user, info: %{is_admin: true}) + moderator = insert(:user, info: %{is_moderator: true}) + subject1 = insert(:user) + subject2 = insert(:user) + + [admin: admin, moderator: moderator, subject1: subject1, subject2: subject2] + end + + test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "delete" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} deleted user @#{subject1.nickname}" + end + + test "logging user creation by moderator", %{ + moderator: moderator, + subject1: subject1, + subject2: subject2 + } do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subjects: [subject1, subject2], + action: "create" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}" + end + + test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "follow" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}" + end + + test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + followed: subject1, + follower: subject2, + action: "unfollow" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}" + end + + test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "tag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" + end + + test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: admin, + nicknames: [subject1.nickname, subject2.nickname], + tags: ["foo", "bar"], + action: "untag" + }) + + log = Repo.one(ModerationLog) + + users = + [subject1.nickname, subject2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" + end + + test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "grant", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} made @#{subject1.nickname} moderator" + end + + test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + subject: subject1, + action: "revoke", + permission: "moderator" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}" + end + + test "logging relay follow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_follow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} followed relay: https://example.org/relay" + end + + test "logging relay unfollow", %{moderator: moderator} do + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "relay_unfollow", + target: "https://example.org/relay" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} unfollowed relay: https://example.org/relay" + end + + test "logging report update", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Flag", + "state" => "resolved" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_update", + subject: report + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state" + end + + test "logging report response", %{moderator: moderator} do + report = %Activity{ + id: "9m9I1F4p8ftrTP6QTI", + data: %{ + "type" => "Note" + } + } + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "report_response", + subject: report, + text: "look at this" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" + end + + test "logging status sensitivity update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: nil + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'" + end + + test "logging status visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: nil, + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'" + end + + test "logging status sensitivity & visibility update", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_update", + subject: note, + sensitive: "true", + visibility: "private" + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'" + end + + test "logging status deletion", %{moderator: moderator} do + note = insert(:note_activity) + + {:ok, _} = + ModerationLog.insert_log(%{ + actor: moderator, + action: "status_delete", + subject_id: note.id + }) + + log = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log) == + "@#{moderator.nickname} deleted status ##{note.id}" + end + end +end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 0ca87f035..895a73d2c 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -159,32 +159,28 @@ defmodule Pleroma.Object.FetcherTest do end describe "signed fetches" do + clear_config([:activitypub, :sign_object_fetches]) + test_with_mock "it signs fetches when configured to do so", Pleroma.Signature, [:passthrough], [] do - option = Pleroma.Config.get([:activitypub, :sign_object_fetches]) Pleroma.Config.put([:activitypub, :sign_object_fetches], true) Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") assert called(Pleroma.Signature.sign(:_, :_)) - - Pleroma.Config.put([:activitypub, :sign_object_fetches], option) end test_with_mock "it doesn't sign fetches when not configured to do so", Pleroma.Signature, [:passthrough], [] do - option = Pleroma.Config.get([:activitypub, :sign_object_fetches]) Pleroma.Config.put([:activitypub, :sign_object_fetches], false) Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") refute called(Pleroma.Signature.sign(:_, :_)) - - Pleroma.Config.put([:activitypub, :sign_object_fetches], option) end end end diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs index ce5d77ff7..d45662a2a 100644 --- a/test/plugs/ensure_public_or_authenticated_plug_test.exs +++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs @@ -9,8 +9,10 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.User + clear_config([:instance, :public]) + test "it halts if not public and no user is assigned", %{conn: conn} do - set_public_to(false) + Config.put([:instance, :public], false) conn = conn @@ -21,7 +23,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do end test "it continues if public", %{conn: conn} do - set_public_to(true) + Config.put([:instance, :public], true) ret_conn = conn @@ -31,7 +33,7 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do end test "it continues if a user is assigned, even if not public", %{conn: conn} do - set_public_to(false) + Config.put([:instance, :public], false) conn = conn @@ -43,13 +45,4 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do assert ret_conn == conn end - - defp set_public_to(value) do - orig = Config.get!([:instance, :public]) - Config.put([:instance, :public], value) - - on_exit(fn -> - Config.put([:instance, :public], orig) - end) - end end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 7dfd50c1f..7a2835e3d 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -7,17 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do alias Pleroma.Config alias Plug.Conn + clear_config([:http_securiy, :enabled]) + clear_config([:http_security, :sts]) + describe "http security enabled" do setup do - enabled = Config.get([:http_securiy, :enabled]) - Config.put([:http_security, :enabled], true) - - on_exit(fn -> - Config.put([:http_security, :enabled], enabled) - end) - - :ok end test "it sends CSP headers when enabled", %{conn: conn} do @@ -81,14 +76,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do end test "it does not send CSP headers when disabled", %{conn: conn} do - enabled = Config.get([:http_securiy, :enabled]) - Config.put([:http_security, :enabled], false) - on_exit(fn -> - Config.put([:http_security, :enabled], enabled) - end) - conn = get(conn, "/api/v1/instance") assert Conn.get_resp_header(conn, "x-xss-protection") == [] diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs index e2dcfa3d8..6aabc45a4 100644 --- a/test/plugs/instance_static_test.exs +++ b/test/plugs/instance_static_test.exs @@ -8,14 +8,12 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do @dir "test/tmp/instance_static" setup do - static_dir = Pleroma.Config.get([:instance, :static_dir]) - Pleroma.Config.put([:instance, :static_dir], @dir) File.mkdir_p!(@dir) + on_exit(fn -> File.rm_rf(@dir) end) + end - on_exit(fn -> - Pleroma.Config.put([:instance, :static_dir], static_dir) - File.rm_rf(@dir) - end) + clear_config([:instance, :static_dir]) do + Pleroma.Config.put([:instance, :static_dir], @dir) end test "overrides index" do diff --git a/test/reverse_proxy_test.exs b/test/reverse_proxy_test.exs index f4b7d6add..3a83c4c48 100644 --- a/test/reverse_proxy_test.exs +++ b/test/reverse_proxy_test.exs @@ -108,11 +108,11 @@ defmodule Pleroma.ReverseProxyTest do end end - test "max_body_size returns error if streaming body more than that option", %{conn: conn} do + test "max_body_length returns error if streaming body more than that option", %{conn: conn} do stream_mock(3, true) assert capture_log(fn -> - ReverseProxy.call(conn, "/stream-bytes/50", max_body_size: 30) + ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30) end) =~ "[warn] Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large" end diff --git a/test/signature_test.exs b/test/signature_test.exs index 26337eaf9..d5bf63d7d 100644 --- a/test/signature_test.exs +++ b/test/signature_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock + import Mock alias Pleroma.Signature @@ -114,4 +115,17 @@ defmodule Pleroma.SignatureTest do "https://example.com/users/1234" end end + + describe "signed_date" do + test "it returns formatted current date" do + with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do + assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT" + end + end + + test "it returns formatted date" do + assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) == + "Fri, 23 Aug 2019 08:11:24 GMT" + end + end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 1787c1088..62d1de717 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors +# Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Factory do @@ -143,6 +143,25 @@ defmodule Pleroma.Factory do |> Map.merge(attrs) end + defp expiration_offset_by_minutes(attrs, minutes) do + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(minutes), :millisecond) + |> NaiveDateTime.truncate(:second) + + %Pleroma.ActivityExpiration{} + |> Map.merge(attrs) + |> Map.put(:scheduled_at, scheduled_at) + end + + def expiration_in_the_past_factory(attrs \\ %{}) do + expiration_offset_by_minutes(attrs, -60) + end + + def expiration_in_the_future_factory(attrs \\ %{}) do + expiration_offset_by_minutes(attrs, 61) + end + def article_activity_factory do article = insert(:article) diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 1a92be065..a601b3ec8 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -7,8 +7,52 @@ defmodule Pleroma.Tests.Helpers do Helpers for use in tests. """ + defmacro clear_config(config_path) do + quote do + clear_config(unquote(config_path)) do + end + end + end + + defmacro clear_config(config_path, do: yield) do + quote do + setup do + initial_setting = Pleroma.Config.get(unquote(config_path)) + unquote(yield) + on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end) + :ok + end + end + end + + defmacro clear_config_all(config_path) do + quote do + clear_config_all(unquote(config_path)) do + end + end + end + + defmacro clear_config_all(config_path, do: yield) do + quote do + setup_all do + initial_setting = Pleroma.Config.get(unquote(config_path)) + unquote(yield) + on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end) + :ok + end + end + end + defmacro __using__(_opts) do quote do + import Pleroma.Tests.Helpers, + only: [ + clear_config: 1, + clear_config: 2, + clear_config_all: 1, + clear_config_all: 2 + ] + def collect_ids(collection) do collection |> Enum.map(& &1.id) @@ -30,6 +74,15 @@ defmodule Pleroma.Tests.Helpers do |> Poison.encode!() |> Poison.decode!() end + + defmacro guards_config(config_path) do + quote do + initial_setting = Pleroma.Config.get(config_path) + + Pleroma.Config.put(config_path, true) + on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) + end + end end end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 3adb5ba3b..55b141dd8 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -17,9 +17,12 @@ defmodule HttpRequestMock do with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do res else - {_, _r} = error -> - # Logger.warn(r) - error + error -> + with {:error, message} <- error do + Logger.warn(message) + end + + {_, _r} = error end end @@ -968,9 +971,25 @@ defmodule HttpRequestMock do }} end + def get("http://example.com/rel_me/anchor", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}} + end + + def get("http://example.com/rel_me/anchor_nofollow", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}} + end + + def get("http://example.com/rel_me/link", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}} + end + + def get("http://example.com/rel_me/null", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}} + end + def get(url, query, body, headers) do {:error, - "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ + "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ inspect(headers) }"} end @@ -1032,7 +1051,10 @@ defmodule HttpRequestMock do }} end - def post(url, _query, _body, _headers) do - {:error, "Not implemented the mock response for post #{inspect(url)}"} + def post(url, query, body, headers) do + {:error, + "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{ + inspect(headers) + }"} end end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index a9b79eb5b..9cd47380c 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -11,21 +11,20 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do Mix.shell(Mix.Shell.Process) temp_file = "config/temp.exported_from_db.secret.exs" - dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) - - Pleroma.Config.put([:instance, :dynamic_configuration], true) - on_exit(fn -> Mix.shell(Mix.Shell.IO) Application.delete_env(:pleroma, :first_setting) Application.delete_env(:pleroma, :second_setting) - Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) :ok = File.rm(temp_file) end) {:ok, temp_file: temp_file} end + clear_config_all([:instance, :dynamic_configuration]) do + Pleroma.Config.put([:instance, :dynamic_configuration], true) + end + test "settings are migrated to db" do assert Repo.all(Config) == [] diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs index a8f25f500..a9925c361 100644 --- a/test/tasks/database_test.exs +++ b/test/tasks/database_test.exs @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.DatabaseTest do + alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -22,6 +23,52 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do :ok end + describe "running remove_embedded_objects" do + test "it replaces objects with references" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) + new_data = Map.put(activity.data, "object", activity.object.data) + + {:ok, activity} = + activity + |> Activity.change(%{data: new_data}) + |> Repo.update() + + assert is_map(activity.data["object"]) + + Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"]) + + activity = Activity.get_by_id_with_object(activity.id) + assert is_binary(activity.data["object"]) + end + end + + describe "prune_objects" do + test "it prunes old objects from the database" do + insert(:note) + deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 + + date = + Timex.now() + |> Timex.shift(days: -deadline) + |> Timex.to_naive_datetime() + |> NaiveDateTime.truncate(:second) + + %{id: id} = + :note + |> insert() + |> Ecto.Changeset.change(%{inserted_at: date}) + |> Repo.update!() + + assert length(Repo.all(Object)) == 2 + + Mix.Tasks.Pleroma.Database.run(["prune_objects"]) + + assert length(Repo.all(Object)) == 1 + refute Object.get_by_id(id) + end + end + describe "running update_users_following_followers_counts" do test "following and followers count are updated" do [user, user2] = insert_pair(:user) diff --git a/test/mix/tasks/pleroma.digest_test.exs b/test/tasks/digest_test.exs similarity index 95% rename from test/mix/tasks/pleroma.digest_test.exs rename to test/tasks/digest_test.exs index 595f64ed7..4bfa1fb93 100644 --- a/test/mix/tasks/pleroma.digest_test.exs +++ b/test/tasks/digest_test.exs @@ -44,7 +44,7 @@ defmodule Mix.Tasks.Pleroma.DigestTest do assert_email_sent( to: {user2.name, user2.email}, - html_body: ~r/new mentions:/i + html_body: ~r/here is what you've missed!/i ) end end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 9d260da3e..7bde56606 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -50,7 +50,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do %User{ap_id: follower_id} = local_user = Relay.get_actor() target_user = User.get_cached_by_ap_id(target_instance) follow_activity = Utils.fetch_latest_follow(local_user, target_user) - + User.follow(local_user, target_user) + assert "#{target_instance}/followers" in refresh_record(local_user).following Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) @@ -67,6 +68,30 @@ defmodule Mix.Tasks.Pleroma.RelayTest do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id assert undo_activity.data["object"] == cancelled_activity.data + refute "#{target_instance}/followers" in refresh_record(local_user).following + end + end + + describe "mix pleroma.relay list" do + test "Prints relay subscription list" do + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + refute_receive {:mix_shell, :info, _} + + 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() + + :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) + + assert_receive {:mix_shell, :info, ["test-app.com"]} + assert_receive {:mix_shell, :info, ["test-app-42.com"]} end end end diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs index 78a3f17b4..917df2675 100644 --- a/test/tasks/robots_txt_test.exs +++ b/test/tasks/robots_txt_test.exs @@ -4,17 +4,17 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do use ExUnit.Case + use Pleroma.Tests.Helpers alias Mix.Tasks.Pleroma.RobotsTxt + clear_config([:instance, :static_dir]) + test "creates new dir" do path = "test/fixtures/new_dir/" file_path = path <> "robots.txt" - - static_dir = Pleroma.Config.get([:instance, :static_dir]) Pleroma.Config.put([:instance, :static_dir], path) on_exit(fn -> - Pleroma.Config.put([:instance, :static_dir], static_dir) {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path) end) @@ -29,11 +29,9 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do test "to existance folder" do path = "test/fixtures/" file_path = path <> "robots.txt" - static_dir = Pleroma.Config.get([:instance, :static_dir]) Pleroma.Config.put([:instance, :static_dir], path) on_exit(fn -> - Pleroma.Config.put([:instance, :static_dir], static_dir) :ok = File.rm(file_path) end) diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs index a31b38ab1..6b33e7395 100644 --- a/test/upload/filter/anonymize_filename_test.exs +++ b/test/upload/filter/anonymize_filename_test.exs @@ -9,12 +9,6 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do alias Pleroma.Upload setup do - custom_filename = Config.get([Upload.Filter.AnonymizeFilename, :text]) - - on_exit(fn -> - Config.put([Upload.Filter.AnonymizeFilename, :text], custom_filename) - end) - upload_file = %Upload{ name: "an… image.jpg", content_type: "image/jpg", @@ -24,6 +18,8 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do %{upload_file: upload_file} end + clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) + test "it replaces filename on pre-defined text", %{upload_file: upload_file} do Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png") {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file) diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs index c301440fd..210320d30 100644 --- a/test/upload/filter/mogrify_test.exs +++ b/test/upload/filter/mogrify_test.exs @@ -10,13 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do alias Pleroma.Upload alias Pleroma.Upload.Filter - setup do - filter = Config.get([Filter.Mogrify, :args]) - - on_exit(fn -> - Config.put([Filter.Mogrify, :args], filter) - end) - end + clear_config([Filter.Mogrify, :args]) test "apply mogrify filter" do Config.put([Filter.Mogrify, :args], [{"tint", "40"}]) diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs index 640cd7107..03887c06a 100644 --- a/test/upload/filter_test.exs +++ b/test/upload/filter_test.exs @@ -8,13 +8,7 @@ defmodule Pleroma.Upload.FilterTest do alias Pleroma.Config alias Pleroma.Upload.Filter - setup do - custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text]) - - on_exit(fn -> - Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename) - end) - end + clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text]) test "applies filters" do Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png") diff --git a/test/upload_test.exs b/test/upload_test.exs index 95b16078b..6721fe82e 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -250,12 +250,8 @@ defmodule Pleroma.UploadTest do end describe "Setting a custom base_url for uploaded media" do - setup do + clear_config([Pleroma.Upload, :base_url]) do Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social") - - on_exit(fn -> - Pleroma.Config.put([Pleroma.Upload, :base_url], nil) - end) end test "returns a media url with configured base_url" do diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs index a0a1cfdf0..171316340 100644 --- a/test/uploaders/s3_test.exs +++ b/test/uploaders/s3_test.exs @@ -11,19 +11,11 @@ defmodule Pleroma.Uploaders.S3Test do import Mock import ExUnit.CaptureLog - setup do - config = Config.get([Pleroma.Uploaders.S3]) - + clear_config([Pleroma.Uploaders.S3]) do Config.put([Pleroma.Uploaders.S3], bucket: "test_bucket", public_endpoint: "https://s3.amazonaws.com" ) - - on_exit(fn -> - Config.put([Pleroma.Uploaders.S3], config) - end) - - :ok end describe "get_file/1" do diff --git a/test/user_test.exs b/test/user_test.exs index b363b322c..2cbc1f525 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -21,6 +21,8 @@ defmodule Pleroma.UserTest do :ok end + clear_config([:instance, :account_activation_required]) + describe "when tags are nil" do test "tagging a user" do user = insert(:user, %{tags: nil}) @@ -90,6 +92,17 @@ defmodule Pleroma.UserTest do assert activity end + test "clears follow requests when requester is blocked" do + followed = insert(:user, %{info: %{locked: true}}) + follower = insert(:user) + + CommonAPI.follow(follower, followed) + assert {:ok, [_activity]} = User.get_follow_requests(followed) + + {:ok, _follower} = User.block(followed, follower) + assert {:ok, []} = User.get_follow_requests(followed) + end + test "follow_all follows mutliple users" do user = insert(:user) followed_zero = insert(:user) @@ -192,24 +205,64 @@ defmodule Pleroma.UserTest do # assert websub # end - test "unfollow takes a user and another user" do - followed = insert(:user) - user = insert(:user, %{following: [User.ap_followers(followed)]}) + describe "unfollow/2" do + setup do + setting = Pleroma.Config.get([:instance, :external_user_synchronization]) - {:ok, user, _activity} = User.unfollow(user, followed) + on_exit(fn -> + Pleroma.Config.put([:instance, :external_user_synchronization], setting) + end) - user = User.get_cached_by_id(user.id) + :ok + end - assert user.following == [] - end + test "unfollow with syncronizes external user" do + Pleroma.Config.put([:instance, :external_user_synchronization], true) - test "unfollow doesn't unfollow yourself" do - user = insert(:user) + followed = + insert(:user, + nickname: "fuser1", + follower_address: "http://localhost:4001/users/fuser1/followers", + following_address: "http://localhost:4001/users/fuser1/following", + ap_id: "http://localhost:4001/users/fuser1" + ) - {:error, _} = User.unfollow(user, user) + user = + insert(:user, %{ + local: false, + nickname: "fuser2", + ap_id: "http://localhost:4001/users/fuser2", + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following", + following: [User.ap_followers(followed)] + }) - user = User.get_cached_by_id(user.id) - assert user.following == [user.ap_id] + {:ok, user, _activity} = User.unfollow(user, followed) + + user = User.get_cached_by_id(user.id) + + assert user.following == [] + end + + test "unfollow takes a user and another user" do + followed = insert(:user) + user = insert(:user, %{following: [User.ap_followers(followed)]}) + + {:ok, user, _activity} = User.unfollow(user, followed) + + user = User.get_cached_by_id(user.id) + + assert user.following == [] + end + + test "unfollow doesn't unfollow yourself" do + user = insert(:user) + + {:error, _} = User.unfollow(user, user) + + user = User.get_cached_by_id(user.id) + assert user.following == [user.ap_id] + end end test "test if a user is following another user" do @@ -236,6 +289,9 @@ defmodule Pleroma.UserTest do password_confirmation: "test", email: "email@example.com" } + clear_config([:instance, :autofollowed_nicknames]) + clear_config([:instance, :welcome_message]) + clear_config([:instance, :welcome_user_nickname]) test "it autofollows accounts that are set for it" do user = insert(:user) @@ -252,8 +308,6 @@ defmodule Pleroma.UserTest do assert User.following?(registered_user, user) refute User.following?(registered_user, remote_user) - - Pleroma.Config.put([:instance, :autofollowed_nicknames], []) end test "it sends a welcome message if it is set" do @@ -269,9 +323,6 @@ defmodule Pleroma.UserTest do assert registered_user.ap_id in activity.recipients assert Object.normalize(activity).data["content"] =~ "cool site" assert activity.actor == welcome_user.ap_id - - Pleroma.Config.put([:instance, :welcome_user_nickname], nil) - Pleroma.Config.put([:instance, :welcome_message], nil) end test "it requires an email, name, nickname and password, bio is optional" do @@ -337,15 +388,8 @@ defmodule Pleroma.UserTest do email: "email@example.com" } - setup do - setting = Pleroma.Config.get([:instance, :account_activation_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) - end - - :ok + clear_config([:instance, :account_activation_required]) do + Pleroma.Config.put([:instance, :account_activation_required], true) end test "it creates unconfirmed user" do @@ -497,6 +541,9 @@ defmodule Pleroma.UserTest do avatar: %{some: "avatar"} } + clear_config([:instance, :user_bio_length]) + clear_config([:instance, :user_name_length]) + test "it confirms validity" do cs = User.remote_user_creation(@valid_remote) assert cs.valid? @@ -992,6 +1039,8 @@ defmodule Pleroma.UserTest do [user: user] end + clear_config([:instance, :federating]) + test ".delete_user_activities deletes all create activities", %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) @@ -1001,6 +1050,13 @@ defmodule Pleroma.UserTest do refute Activity.get_by_id(activity.id) end + test "it deletes deactivated user" do + {:ok, user} = insert(:user, info: %{deactivated: true}) |> User.set_cache() + + assert {:ok, _} = User.delete(user) + refute User.get_by_id(user.id) + end + test "it deletes a user, all follow relationships and all activities", %{user: user} do follower = insert(:user) {:ok, follower} = User.follow(follower, user) @@ -1042,9 +1098,7 @@ defmodule Pleroma.UserTest do Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - Pleroma.Config.put(config_path, true) + Pleroma.Config.put([:instance, :federating], true) {:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") {:ok, _} = User.follow(follower, user) @@ -1056,8 +1110,6 @@ defmodule Pleroma.UserTest do inbox: "http://mastodon.example.org/inbox" }) ) - - Pleroma.Config.put(config_path, initial_setting) end end @@ -1123,8 +1175,6 @@ defmodule Pleroma.UserTest do refute User.auth_active?(local_user) assert User.auth_active?(confirmed_user) assert User.auth_active?(remote_user) - - Pleroma.Config.put([:instance, :account_activation_required], false) end describe "superuser?/1" do @@ -1169,8 +1219,6 @@ defmodule Pleroma.UserTest do other_user = insert(:user, local: true) refute User.visible_for?(user, other_user) - - Pleroma.Config.put([:instance, :account_activation_required], false) end test "returns true when the account is unauthenticated and auth is not required" do @@ -1187,8 +1235,6 @@ defmodule Pleroma.UserTest do other_user = insert(:user, local: true, info: %{is_admin: true}) assert User.visible_for?(user, other_user) - - Pleroma.Config.put([:instance, :account_activation_required], false) end end @@ -1207,18 +1253,18 @@ defmodule Pleroma.UserTest do end test "Adds rel=me on linkbacked urls" do - user = insert(:user, ap_id: "http://social.example.org/users/lain") + user = insert(:user, ap_id: "https://social.example.org/users/lain") - bio = "http://example.org/rel_me/null" + bio = "http://example.com/rel_me/null" expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) - bio = "http://example.org/rel_me/link" - expected_text = "#{bio}" + bio = "http://example.com/rel_me/link" + expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) - bio = "http://example.org/rel_me/anchor" - expected_text = "#{bio}" + bio = "http://example.com/rel_me/anchor" + expected_text = "#{bio}" assert expected_text == User.parse_bio(bio, user) end end @@ -1501,10 +1547,7 @@ defmodule Pleroma.UserTest do end describe "following/followers synchronization" do - setup do - sync = Pleroma.Config.get([:instance, :external_user_synchronization]) - on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end) - end + clear_config([:instance, :external_user_synchronization]) test "updates the counters normally on following/getting a follow when disabled" do Pleroma.Config.put([:instance, :external_user_synchronization], false) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 251055ee1..5192e734f 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -10,23 +10,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - :ok end + clear_config_all([:instance, :federating], + do: Pleroma.Config.put([:instance, :federating], true) + ) + describe "/relay" do + clear_config([:instance, :allow_relay]) + test "with the relay active, it returns the relay user", %{conn: conn} do res = conn @@ -43,8 +43,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> get(activity_pub_path(conn, :relay)) |> json_response(404) |> assert - - Pleroma.Config.put([:instance, :allow_relay], true) end end @@ -596,6 +594,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do end end + describe "/relay/followers" do + test "it returns relay followers", %{conn: conn} do + relay_actor = Relay.get_actor() + user = insert(:user) + User.follow(user, relay_actor) + + result = + conn + |> assign(:relay, true) + |> get("/relay/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [user.ap_id] + end + end + + describe "/relay/following" do + test "it returns relay following", %{conn: conn} do + result = + conn + |> assign(:relay, true) + |> get("/relay/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + end + end + describe "/users/:nickname/followers" do test "it returns the followers in a collection", %{conn: conn} do user = insert(:user) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index d723f331f..1515f4eb6 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -538,6 +538,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert Enum.member?(activities, activity_one) end + test "doesn't return thread muted activities" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user}) + end + + test "returns thread muted activities when with_muted is set" do + user = insert(:user) + _activity_one = insert(:note_activity) + note_two = insert(:note, data: %{"context" => "suya.."}) + activity_two = insert(:note_activity, note: note_two) + + {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two) + + assert [_activity_two, _activity_one] = + ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true}) + end + test "does include announces on request" do activity_three = insert(:note_activity) user = insert(:user) diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index 19e172939..04709df17 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -1,5 +1,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do use ExUnit.Case, async: true + use Pleroma.Tests.Helpers alias Pleroma.Web.ActivityPub.MRF test "subdomains_regex/1" do @@ -59,6 +60,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do end describe "describe/0" do + clear_config([:instance, :rewrite_policy]) + test "it works as expected with noop policy" do expected = %{ mrf_policies: ["NoOpPolicy"], @@ -69,7 +72,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do end test "it works as expected with mock policy" do - config = Pleroma.Config.get([:instance, :rewrite_policy]) Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock]) expected = %{ @@ -79,8 +81,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do } {:ok, ^expected} = MRF.describe() - - Pleroma.Config.put([:instance, :rewrite_policy], config) end end end diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs index fdf6b245e..fc1d190bb 100644 --- a/test/web/activity_pub/mrf/reject_non_public_test.exs +++ b/test/web/activity_pub/mrf/reject_non_public_test.exs @@ -8,12 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic - setup do - policy = Pleroma.Config.get([:mrf_rejectnonpublic]) - on_exit(fn -> Pleroma.Config.put([:mrf_rejectnonpublic], policy) end) - - :ok - end + clear_config([:mrf_rejectnonpublic]) describe "public message" do test "it's allowed when address is public" do diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs index 8e86d2219..7203b27da 100644 --- a/test/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/web/activity_pub/mrf/simple_policy_test.exs @@ -8,9 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do alias Pleroma.Config alias Pleroma.Web.ActivityPub.MRF.SimplePolicy - setup do - orig = Config.get!(:mrf_simple) - + clear_config([:mrf_simple]) do Config.put(:mrf_simple, media_removal: [], media_nsfw: [], @@ -21,10 +19,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do avatar_removal: [], banner_removal: [] ) - - on_exit(fn -> - Config.put(:mrf_simple, orig) - end) end describe "when :media_removal" do diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs index 6519e2398..72084c0fd 100644 --- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs +++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs @@ -7,12 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy - setup do - policy = Pleroma.Config.get([:mrf_user_allowlist]) || [] - on_exit(fn -> Pleroma.Config.put([:mrf_user_allowlist], policy) end) - - :ok - end + clear_config([:mrf_user_allowlist, :localhost]) test "pass filter if allow list is empty" do actor = insert(:user) diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs index c3b11d7a1..38309f9f1 100644 --- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs +++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs @@ -8,8 +8,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy describe "accept" do + clear_config([:mrf_vocabulary, :accept]) + test "it accepts based on parent activity type" do - config = Pleroma.Config.get([:mrf_vocabulary, :accept]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"]) message = %{ @@ -18,12 +19,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:ok, ^message} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :accept], config) end test "it accepts based on child object type" do - config = Pleroma.Config.get([:mrf_vocabulary, :accept]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) message = %{ @@ -35,12 +33,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:ok, ^message} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :accept], config) end test "it does not accept disallowed child objects" do - config = Pleroma.Config.get([:mrf_vocabulary, :accept]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"]) message = %{ @@ -52,12 +47,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:reject, nil} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :accept], config) end test "it does not accept disallowed parent types" do - config = Pleroma.Config.get([:mrf_vocabulary, :accept]) Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"]) message = %{ @@ -69,14 +61,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:reject, nil} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :accept], config) end end describe "reject" do + clear_config([:mrf_vocabulary, :reject]) + test "it rejects based on parent activity type" do - config = Pleroma.Config.get([:mrf_vocabulary, :reject]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) message = %{ @@ -85,12 +76,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:reject, nil} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :reject], config) end test "it rejects based on child object type" do - config = Pleroma.Config.get([:mrf_vocabulary, :reject]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"]) message = %{ @@ -102,12 +90,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:reject, nil} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :reject], config) end test "it passes through objects that aren't disallowed" do - config = Pleroma.Config.get([:mrf_vocabulary, :reject]) Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"]) message = %{ @@ -116,8 +101,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do } {:ok, ^message} = VocabularyPolicy.filter(message) - - Pleroma.Config.put([:mrf_vocabulary, :reject], config) end end end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index e10b808f7..4f7d592a6 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do alias Pleroma.Web.ActivityPub.Relay import Pleroma.Factory + import Mock test "gets an actor for the relay" do user = Relay.get_actor() @@ -43,16 +44,21 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do user = insert(:user) service_actor = Relay.get_actor() ActivityPub.follow(service_actor, user) + Pleroma.User.follow(service_actor, user) + assert "#{user.ap_id}/followers" in refresh_record(service_actor).following assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" assert user.ap_id in activity.recipients assert activity.data["type"] == "Undo" assert activity.data["actor"] == service_actor.ap_id assert activity.data["to"] == [user.ap_id] + refute "#{user.ap_id}/followers" in refresh_record(service_actor).following end end describe "publish/1" do + clear_config([:instance, :federating]) + test "returns error when activity not `Create` type" do activity = insert(:like_activity) assert Relay.publish(activity) == {:error, "Not implemented"} @@ -63,13 +69,44 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do assert Relay.publish(activity) == {:error, false} end - test "returns announce activity" do + test "returns error when object is unknown" do + activity = + insert(:note_activity, + data: %{ + "type" => "Create", + "object" => "http://mastodon.example.org/eee/99541947525187367" + } + ) + + assert Relay.publish(activity) == {:error, nil} + end + + test_with_mock "returns announce activity and publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + Pleroma.Config.put([:instance, :federating], true) service_actor = Relay.get_actor() note = insert(:note_activity) assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) assert activity.data["type"] == "Announce" assert activity.data["actor"] == service_actor.ap_id assert activity.data["object"] == obj.data["id"] + assert called(Pleroma.Web.Federator.publish(activity, 5)) + end + + test_with_mock "returns announce activity and not publish to federate", + Pleroma.Web.Federator, + [:passthrough], + [] do + Pleroma.Config.put([:instance, :federating], false) + service_actor = Relay.get_actor() + note = insert(:note_activity) + assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) + assert activity.data["type"] == "Announce" + assert activity.data["actor"] == service_actor.ap_id + assert activity.data["object"] == obj.data["id"] + refute called(Pleroma.Web.Federator.publish(activity, 5)) end end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 060b91e29..0661d5d7c 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -24,6 +24,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :ok end + clear_config([:instance, :max_remote_account_fields]) + describe "handle_incoming" do test "it ignores an incoming notice if we already have it" do activity = insert(:note_activity) @@ -509,6 +511,68 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert user.bio == "

Some bio

" end + test "it works with custom profile fields" do + {:ok, activity} = + "test/fixtures/mastodon-post-activity.json" + |> File.read!() + |> Poison.decode!() + |> Transmogrifier.handle_incoming() + + user = User.get_cached_by_ap_id(activity.actor) + + assert User.Info.fields(user.info) == [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo1", "value" => "bar1"} + ] + + update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!() + + object = + update_data["object"] + |> Map.put("actor", user.ap_id) + |> Map.put("id", user.ap_id) + + update_data = + update_data + |> Map.put("actor", user.ap_id) + |> Map.put("object", object) + + {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert User.Info.fields(user.info) == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + Pleroma.Config.put([:instance, :max_remote_account_fields], 2) + + update_data = + put_in(update_data, ["object", "attachment"], [ + %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}, + %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"}, + %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"} + ]) + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert User.Info.fields(user.info) == [ + %{"name" => "foo", "value" => "updated"}, + %{"name" => "foo1", "value" => "updated"} + ] + + update_data = put_in(update_data, ["object", "attachment"], []) + + {:ok, _} = Transmogrifier.handle_incoming(update_data) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert User.Info.fields(user.info) == [] + end + test "it works for incoming update activities which lock the account" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 86254117f..fb7fd9e79 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -22,6 +22,21 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") end + test "Renders profile fields" do + fields = [ + %{"name" => "foo", "value" => "bar"} + ] + + {:ok, user} = + insert(:user) + |> User.upgrade_changeset(%{info: %{fields: fields}}) + |> User.update_and_set_cache() + + assert %{ + "attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}] + } = UserView.render("user.json", %{user: user}) + end + test "Does not add an avatar image if the user hasn't set one" do user = insert(:user) {:ok, user} = User.ensure_keys_present(user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index bcbc18639..4e2c27431 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.Activity alias Pleroma.HTML + alias Pleroma.ModerationLog + alias Pleroma.Repo alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.CommonAPI @@ -24,6 +26,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") + 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}" + assert json_response(conn, 200) == user.nickname end @@ -35,12 +45,135 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> assign(:user, admin) |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ - "nickname" => "lain", - "email" => "lain@example.org", - "password" => "test" + "users" => [ + %{ + "nickname" => "lain", + "email" => "lain@example.org", + "password" => "test" + }, + %{ + "nickname" => "lain2", + "email" => "lain2@example.org", + "password" => "test" + } + ] }) - assert json_response(conn, 200) == "lain" + response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) + assert response == ["success", "success"] + + log_entry = Repo.one(ModerationLog) + + assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] + end + + test "Cannot create user with exisiting email" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + } + ] + end + + test "Cannot create user with exisiting nickname" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => user.nickname, + "email" => "someuser@plerama.social", + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => "someuser@plerama.social", + "nickname" => user.nickname + }, + "error" => "nickname has already been taken", + "type" => "error" + } + ] + end + + test "Multiple user creation works in transaction" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users", %{ + "users" => [ + %{ + "nickname" => "newuser", + "email" => "newuser@pleroma.social", + "password" => "test" + }, + %{ + "nickname" => "lain", + "email" => user.email, + "password" => "test" + } + ] + }) + + assert json_response(conn, 409) == [ + %{ + "code" => 409, + "data" => %{ + "email" => user.email, + "nickname" => "lain" + }, + "error" => "email has already been taken", + "type" => "error" + }, + %{ + "code" => 409, + "data" => %{ + "email" => "newuser@pleroma.social", + "nickname" => "newuser" + }, + "error" => "", + "type" => "error" + } + ] + + assert User.get_by_nickname("newuser") === nil end end @@ -99,6 +232,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do follower = User.get_cached_by_id(follower.id) assert User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}" end end @@ -122,6 +260,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do follower = User.get_cached_by_id(follower.id) refute User.following?(follower, user) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}" end end @@ -142,17 +285,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do }&tags[]=foo&tags[]=bar" ) - %{conn: conn, user1: user1, user2: user2, user3: user3} + %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} end test "it appends specified tags to users with specified nicknames", %{ conn: conn, + admin: admin, user1: user1, user2: user2 } do assert json_response(conn, :no_content) assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["foo", "bar"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} added tags: #{tags} to users: #{users}" end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do @@ -178,17 +334,30 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do }&tags[]=x&tags[]=z" ) - %{conn: conn, user1: user1, user2: user2, user3: user3} + %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} end test "it removes specified tags from users with specified nicknames", %{ conn: conn, + admin: admin, user1: user1, user2: user2 } do assert json_response(conn, :no_content) assert User.get_cached_by_id(user1.id).tags == [] assert User.get_cached_by_id(user2.id).tags == ["y"] + + log_entry = Repo.one(ModerationLog) + + users = + [user1.nickname, user2.nickname] + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + + tags = ["x", "z"] |> Enum.join(", ") + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} removed tags: #{tags} from users: #{users}" end test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do @@ -226,6 +395,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do 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.nickname} admin" end test "/:right DELETE, can remove from a permission group" do @@ -241,6 +415,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, 200) == %{ "is_admin" => false } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} revoked admin role from @#{user.nickname}" end end @@ -253,10 +432,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> assign(:user, admin) |> put_req_header("accept", "application/json") - %{conn: conn} + %{conn: conn, admin: admin} end - test "deactivates the user", %{conn: conn} do + test "deactivates the user", %{conn: conn, admin: admin} do user = insert(:user) conn = @@ -266,9 +445,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do user = User.get_cached_by_id(user.id) assert user.info.deactivated == true assert json_response(conn, :no_content) + + 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} do + test "activates the user", %{conn: conn, admin: admin} do user = insert(:user, info: %{deactivated: true}) conn = @@ -278,6 +462,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do 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 @@ -294,20 +483,17 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "POST /api/pleroma/admin/email_invite, with valid config" do setup do - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) - Pleroma.Config.put([:instance, :registrations_open], false) - Pleroma.Config.put([:instance, :invites_enabled], true) - - on_exit(fn -> - Pleroma.Config.put([:instance, :registrations_open], registrations_open) - Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) - :ok - end) - [user: insert(:user, info: %{is_admin: true})] end + clear_config([:instance, :registrations_open]) do + Pleroma.Config.put([:instance, :registrations_open], false) + end + + clear_config([:instance, :invites_enabled]) do + Pleroma.Config.put([:instance, :invites_enabled], true) + end + test "sends invitation and returns 204", %{conn: conn, user: user} do recipient_email = "foo@bar.com" recipient_name = "J. D." @@ -360,18 +546,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do [user: insert(:user, info: %{is_admin: true})] end + clear_config([:instance, :registrations_open]) + clear_config([:instance, :invites_enabled]) + test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) Pleroma.Config.put([:instance, :registrations_open], false) Pleroma.Config.put([:instance, :invites_enabled], false) - on_exit(fn -> - Pleroma.Config.put([:instance, :registrations_open], registrations_open) - Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) - :ok - end) - conn = conn |> assign(:user, user) @@ -381,17 +562,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) Pleroma.Config.put([:instance, :registrations_open], true) Pleroma.Config.put([:instance, :invites_enabled], true) - on_exit(fn -> - Pleroma.Config.put([:instance, :registrations_open], registrations_open) - Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) - :ok - end) - conn = conn |> assign(:user, user) @@ -884,6 +1057,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "avatar" => User.avatar_url(user) |> MediaProxy.url(), "display_name" => HTML.strip_tags(user.name || user.nickname) } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated user @#{user.nickname}" end describe "GET /api/pleroma/admin/users/invite_token" do @@ -1069,25 +1247,35 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "status_ids" => [activity.id] }) - %{conn: assign(conn, :user, admin), id: report_id} + %{conn: assign(conn, :user, admin), id: report_id, admin: admin} end - test "mark report as resolved", %{conn: conn, id: id} do + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"}) |> json_response(:ok) assert response["state"] == "resolved" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'resolved' state" end - test "closes report", %{conn: conn, id: id} do + test "closes report", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"}) |> json_response(:ok) assert response["state"] == "closed" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated report ##{id} with 'closed' state" end test "returns 400 when state is unknown", %{conn: conn, id: id} do @@ -1218,14 +1406,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + # describe "POST /api/pleroma/admin/reports/:id/respond" do setup %{conn: conn} do admin = insert(:user, info: %{is_admin: true}) - %{conn: assign(conn, :user, admin)} + %{conn: assign(conn, :user, admin), admin: admin} end - test "returns created dm", %{conn: conn} do + test "returns created dm", %{conn: conn, admin: admin} do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -1248,6 +1437,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert reporter.nickname in recipients assert response["content"] == "I will check it out" assert response["visibility"] == "direct" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} responded with 'I will check it out' to report ##{ + response["id"] + }" end test "returns 400 when status is missing", %{conn: conn} do @@ -1271,10 +1467,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do admin = insert(:user, info: %{is_admin: true}) activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id} + %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} end - test "toggle sensitive flag", %{conn: conn, id: id} do + test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"}) @@ -1282,6 +1478,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert response["sensitive"] + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set sensitive: 'true'" + response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"}) @@ -1290,7 +1491,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do refute response["sensitive"] end - test "change visibility flag", %{conn: conn, id: id} do + test "change visibility flag", %{conn: conn, id: id, admin: admin} do response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"}) @@ -1298,6 +1499,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert response["visibility"] == "public" + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} updated status ##{id}, set visibility: 'public'" + response = conn |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"}) @@ -1327,15 +1533,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do admin = insert(:user, info: %{is_admin: true}) activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id} + %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} end - test "deletes status", %{conn: conn, id: id} do + test "deletes status", %{conn: conn, id: id, admin: admin} do conn |> delete("/api/pleroma/admin/statuses/#{id}") |> json_response(:ok) refute Activity.get_by_id(id) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted status ##{id}" end test "returns error when status is not exist", %{conn: conn} do @@ -1402,17 +1613,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok = File.rm(temp_file) end) - dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) - - Pleroma.Config.put([:instance, :dynamic_configuration], true) - - on_exit(fn -> - Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) - end) - %{conn: assign(conn, :user, admin)} end + clear_config([:instance, :dynamic_configuration]) do + Pleroma.Config.put([:instance, :dynamic_configuration], true) + end + test "create new config setting in db", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/config", %{ @@ -1961,17 +2168,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok = File.rm(temp_file) end) - dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) - - Pleroma.Config.put([:instance, :dynamic_configuration], true) - - on_exit(fn -> - Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) - end) - %{conn: assign(conn, :user, admin), admin: admin} end + clear_config([:instance, :dynamic_configuration]) do + Pleroma.Config.put([:instance, :dynamic_configuration], true) + end + test "transfer settings to DB and to file", %{conn: conn, admin: admin} do assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") @@ -2044,6 +2247,108 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, 200) |> length() == 5 end end + + describe "GET /api/pleroma/admin/moderation_log" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "returns the log", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn = get(conn, "/api/pleroma/admin/moderation_log") + + response = json_response(conn, 200) + [first_entry, second_entry] = response + + assert response |> length() == 2 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + + test "returns the log with pagination", %{conn: conn, admin: admin} do + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_follow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second) + }) + + Repo.insert(%ModerationLog{ + data: %{ + actor: %{ + "id" => admin.id, + "nickname" => admin.nickname, + "type" => "user" + }, + action: "relay_unfollow", + target: "https://example.org/relay" + }, + inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second) + }) + + conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1") + + response1 = json_response(conn1, 200) + [first_entry] = response1 + + assert response1 |> length() == 1 + assert first_entry["data"]["action"] == "relay_unfollow" + + assert first_entry["message"] == + "@#{admin.nickname} unfollowed relay: https://example.org/relay" + + conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2") + + response2 = json_response(conn2, 200) + [second_entry] = response2 + + assert response2 |> length() == 1 + assert second_entry["data"]["action"] == "relay_follow" + + assert second_entry["message"] == + "@#{admin.nickname} followed relay: https://example.org/relay" + end + end end # Needed for testing diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 16b3f121d..f28a66090 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -5,18 +5,66 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI import Pleroma.Factory + clear_config([:instance, :safe_dm_mentions]) + clear_config([:instance, :limit]) + clear_config([:instance, :max_pinned_statuses]) + + test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + + {:ok, convo_reply} = + CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id}) + + assert Visibility.is_direct?(convo_reply) + + assert activity.data["context"] == convo_reply.data["context"] + end + + test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{ + "status" => "@#{jafnhar.nickname} hey", + "visibility" => "direct" + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + + [participation] = Participation.for_user(har) + + {:ok, activity} = + CommonAPI.post(har, %{ + "status" => "I don't really like @#{tridi.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id, + "in_reply_to_conversation_id" => participation.id + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + refute tridi.ap_id in activity.recipients + end + test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do har = insert(:user) jafnhar = insert(:user) tridi = insert(:user) - option = Pleroma.Config.get([:instance, :safe_dm_mentions]) Pleroma.Config.put([:instance, :safe_dm_mentions], true) {:ok, activity} = @@ -27,7 +75,6 @@ defmodule Pleroma.Web.CommonAPITest do refute tridi.ap_id in activity.recipients assert jafnhar.ap_id in activity.recipients - Pleroma.Config.put([:instance, :safe_dm_mentions], option) end test "it de-duplicates tags" do @@ -150,15 +197,27 @@ defmodule Pleroma.Web.CommonAPITest do end test "it returns error when character limit is exceeded" do - limit = Pleroma.Config.get([:instance, :limit]) Pleroma.Config.put([:instance, :limit], 5) user = insert(:user) assert {:error, "The status is over the character limit"} = CommonAPI.post(user, %{"status" => "foobar"}) + end - Pleroma.Config.put([:instance, :limit], limit) + test "it can handle activities that expire" do + user = insert(:user) + + expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.truncate(:second) + |> NaiveDateTime.add(1_000_000, :second) + + assert {:ok, activity} = + CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000}) + + assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id) + assert expiration.scheduled_at == expires_at end end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 5989d7d29..c281dd1f1 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -239,7 +239,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) assert length(to) == 2 assert length(cc) == 1 @@ -256,7 +256,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) assert length(to) == 3 assert length(cc) == 1 @@ -272,7 +272,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) assert length(to) == 2 assert length(cc) == 1 @@ -289,7 +289,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) assert length(to) == 3 assert length(cc) == 1 @@ -305,7 +305,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) assert length(to) == 2 assert length(cc) == 0 @@ -320,7 +320,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) assert length(to) == 3 assert length(cc) == 0 @@ -335,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) assert length(to) == 1 assert length(cc) == 0 @@ -350,7 +350,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) assert length(to) == 2 assert length(cc) == 0 diff --git a/test/web/digest_email_worker_test.exs b/test/web/digest_email_worker_test.exs new file mode 100644 index 000000000..15002330f --- /dev/null +++ b/test/web/digest_email_worker_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.DigestEmailWorkerTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.DigestEmailWorker + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + test "it sends digest emails" do + user = insert(:user) + + date = + Timex.now() + |> Timex.shift(days: -10) + |> Timex.to_naive_datetime() + + user2 = insert(:user, last_digest_emailed_at: date) + User.switch_email_notifications(user2, "digest", true) + CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"}) + + DigestEmailWorker.perform() + + assert_received {:email, email} + assert email.to == [{user2.name, user2.email}] + assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}" + end +end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 73cfaa8f1..09e54533f 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -13,15 +13,17 @@ defmodule Pleroma.Web.FederatorTest do setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - :ok end + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) + end + + clear_config([:instance, :allow_relay]) + clear_config([:instance, :rewrite_policy]) + clear_config([:mrf_keyword]) + describe "Publisher.perform" do test "call `perform` with unknown task" do assert { @@ -67,8 +69,6 @@ defmodule Pleroma.Web.FederatorTest do end refute_received :relay_publish - - Pleroma.Config.put([:instance, :allow_relay], true) end end @@ -231,19 +231,18 @@ defmodule Pleroma.Web.FederatorTest do end test "it does not crash if MRF rejects the post" do - policies = Pleroma.Config.get([:instance, :rewrite_policy]) - mrf_keyword_policy = Pleroma.Config.get(:mrf_keyword) Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) - Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.KeywordPolicy) + + Pleroma.Config.put( + [:instance, :rewrite_policy], + Pleroma.Web.ActivityPub.MRF.KeywordPolicy + ) params = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() assert Federator.incoming_ap_doc(params) == :error - - Pleroma.Config.put([:instance, :rewrite_policy], policies) - Pleroma.Config.put(:mrf_keyword, mrf_keyword_policy) end end end diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs index d28730994..3fd011fd3 100644 --- a/test/web/instances/instance_test.exs +++ b/test/web/instances/instance_test.exs @@ -10,14 +10,8 @@ defmodule Pleroma.Instances.InstanceTest do import Pleroma.Factory - setup_all do - config_path = [:instance, :federation_reachability_timeout_days] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, 1) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - - :ok + clear_config_all([:instance, :federation_reachability_timeout_days]) do + Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1) end describe "set_reachable/1" do diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs index f0d84edea..dea8e2aea 100644 --- a/test/web/instances/instances_test.exs +++ b/test/web/instances/instances_test.exs @@ -7,14 +7,8 @@ defmodule Pleroma.InstancesTest do use Pleroma.DataCase - setup_all do - config_path = [:instance, :federation_reachability_timeout_days] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, 1) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - - :ok + clear_config_all([:instance, :federation_reachability_timeout_days]) do + Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1) end describe "reachable?/1" do diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index a26f514a5..1d8b28339 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -67,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do source: %{ note: "valid html", sensitive: false, - pleroma: %{} + pleroma: %{}, + fields: [] }, pleroma: %{ background_image: "https://example.com/images/asuka_hospital.png", @@ -134,7 +135,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do source: %{ note: user.bio, sensitive: false, - pleroma: %{} + pleroma: %{}, + fields: [] }, pleroma: %{ background_image: nil, @@ -304,7 +306,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do source: %{ note: user.bio, sensitive: false, - pleroma: %{} + pleroma: %{}, + fields: [] }, pleroma: %{ background_image: nil, diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs new file mode 100644 index 000000000..a2a880705 --- /dev/null +++ b/test/web/mastodon_api/conversation_view_test.exs @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do + use Pleroma.DataCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.ConversationView + + import Pleroma.Factory + + test "represents a Mastodon Conversation entity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"}) + + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation + + conversation = + ConversationView.render("participation.json", %{participation: participation, for: user}) + + assert conversation.id == participation.id |> to_string() + assert conversation.last_status.id == activity.id + + assert [account] = conversation.accounts + assert account.id == other_user.id + end +end diff --git a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs index 71d0c8af8..87ee82050 100644 --- a/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller/update_credentials_test.exs @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do use Pleroma.Web.ConnCase import Pleroma.Factory + clear_config([:instance, :max_account_fields]) describe "updating credentials" do test "sets user settings in a generic way", %{conn: conn} do @@ -300,5 +301,69 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do assert user["display_name"] == name assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"] end + + test "update fields", %{conn: conn} do + user = insert(:user) + + fields = [ + %{"name" => "foo", "value" => ""}, + %{"name" => "link", "value" => "cofe.io"} + ] + + account = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(200) + + assert account["fields"] == [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert account["source"]["fields"] == [ + %{ + "name" => "foo", + "value" => "" + }, + %{"name" => "link", "value" => "cofe.io"} + ] + + name_limit = Pleroma.Config.get([:instance, :account_field_name_length]) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length]) + + long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => "foo", "value" => long_value}] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + + long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join() + + fields = [%{"name" => long_name, "value" => "bar"}] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + + Pleroma.Config.put([:instance, :max_account_fields], 1) + + fields = [ + %{"name" => "foo", "value" => "bar"}, + %{"name" => "link", "value" => "cofe.io"} + ] + + assert %{"error" => "Invalid request"} == + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields}) + |> json_response(403) + end end end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 112e272f9..6fcdc19aa 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Ecto.Changeset alias Pleroma.Activity + alias Pleroma.ActivityExpiration alias Pleroma.Config alias Pleroma.Notification alias Pleroma.Object @@ -33,6 +34,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do :ok end + clear_config([:instance, :public]) + clear_config([:rich_media, :enabled]) + test "the home timeline", %{conn: conn} do user = insert(:user) following = insert(:user) @@ -86,13 +90,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end test "the public timeline when public is set to false", %{conn: conn} do - public = Config.get([:instance, :public]) Config.put([:instance, :public], false) - on_exit(fn -> - Config.put([:instance, :public], public) - end) - assert conn |> get("/api/v1/timelines/public", %{"local" => "False"}) |> json_response(403) == %{"error" => "This resource requires authentication."} @@ -152,6 +151,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => third_id} = json_response(conn_three, 200) refute id == third_id + + # An activity that will expire: + # 2 hours + expires_in = 120 * 60 + + conn_four = + conn + |> post("api/v1/statuses", %{ + "status" => "oolong", + "expires_in" => expires_in + }) + + assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200) + assert activity = Activity.get_by_id(fourth_id) + assert expiration = ActivityExpiration.get_by_activity_id(fourth_id) + + estimated_expires_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(expires_in) + |> NaiveDateTime.truncate(:second) + + # This assert will fail if the test takes longer than a minute. I sure hope it never does: + assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60 + + assert fourth_response["pleroma"]["expires_at"] == + NaiveDateTime.to_iso8601(expiration.scheduled_at) end test "replying to a status", %{conn: conn} do @@ -261,7 +286,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200) assert Activity.get_by_id(id) - Config.put([:rich_media, :enabled], false) end test "posting a direct status", %{conn: conn} do @@ -406,7 +430,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"visibility" => "direct"} = status assert status["url"] != direct.data["id"] - # User should be able to see his own direct message + # User should be able to see their own direct message res_conn = build_conn() |> assign(:user, user_one) @@ -1634,14 +1658,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do describe "media upload" do setup do - upload_config = Config.get([Pleroma.Upload]) - proxy_config = Config.get([:media_proxy]) - - on_exit(fn -> - Config.put([Pleroma.Upload], upload_config) - Config.put([:media_proxy], proxy_config) - end) - user = insert(:user) conn = @@ -1657,6 +1673,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do [conn: conn, image: image] end + clear_config([:media_proxy]) + clear_config([Pleroma.Upload]) + test "returns uploaded image", %{conn: conn, image: image} do desc = "Description of the image" @@ -2667,14 +2686,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do describe "pinned statuses" do setup do - Config.put([:instance, :max_pinned_statuses], 1) - user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) [user: user, activity: activity] end + clear_config([:instance, :max_pinned_statuses]) do + Config.put([:instance, :max_pinned_statuses], 1) + end + test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do {:ok, _} = CommonAPI.pin(activity.id, user) @@ -2769,10 +2790,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do setup do Config.put([:rich_media, :enabled], true) - on_exit(fn -> - Config.put([:rich_media, :enabled], false) - end) - user = insert(:user) %{user: user} end @@ -3127,15 +3144,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do conn: conn, path: path } do - is_public = Config.get([:instance, :public]) Config.put([:instance, :public], false) conn = get(conn, path) assert conn.status == 302 assert redirected_to(conn) == "/web/login" - - Config.put([:instance, :public], is_public) end test "does not redirect logged in users to the login page", %{conn: conn, path: path} do @@ -3910,13 +3924,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do describe "POST /api/v1/pleroma/accounts/confirmation_resend" do setup do - setting = Config.get([:instance, :account_activation_required]) - - unless setting do - Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Config.put([:instance, :account_activation_required], setting) end) - end - user = insert(:user) info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) @@ -3931,6 +3938,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do [user: user] end + clear_config([:instance, :account_activation_required]) do + Config.put([:instance, :account_activation_required], true) + end + test "resend account confirmation email", %{conn: conn, user: user} do conn |> assign(:user, user) @@ -3953,9 +3964,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do setup do user = insert(:user) other_user = insert(:user) - config = Config.get(:suggestions) - on_exit(fn -> Config.put(:suggestions, config) end) - host = Config.get([Pleroma.Web.Endpoint, :url, :host]) url500 = "http://test500?#{host}&#{user.nickname}" url200 = "http://test200?#{host}&#{user.nickname}" @@ -3977,6 +3985,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do [user: user, other_user: other_user] end + clear_config(:suggestions) + test "returns empty result when suggestions disabled", %{conn: conn, user: user} do Config.put([:suggestions, :enabled], false) diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 0b167f839..1b6beb6d2 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -23,6 +23,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do :ok end + test "returns the direct conversation id when given the `with_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + status = + StatusView.render("status.json", + activity: activity, + with_direct_conversation_id: true, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] + end + test "returns a temporary ap_id based user for activities missing db users" do user = insert(:user) @@ -133,7 +148,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do conversation_id: convo_id, in_reply_to_account_acct: nil, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])} + spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, + expires_at: nil, + direct_conversation_id: nil } } diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index 0c94755df..79699cac5 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -4,14 +4,11 @@ defmodule Pleroma.Web.MediaProxyTest do use ExUnit.Case + use Pleroma.Tests.Helpers import Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy.MediaProxyController - setup do - enabled = Pleroma.Config.get([:media_proxy, :enabled]) - on_exit(fn -> Pleroma.Config.put([:media_proxy, :enabled], enabled) end) - :ok - end + clear_config([:media_proxy, :enabled]) describe "when enabled" do setup do diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs index 0eb191c76..1cbe133b7 100644 --- a/test/web/oauth/ldap_authorization_test.exs +++ b/test/web/oauth/ldap_authorization_test.exs @@ -12,21 +12,12 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do @skip if !Code.ensure_loaded?(:eldap), do: :skip - setup_all do - ldap_authenticator = - Pleroma.Config.get(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator) - - ldap_enabled = Pleroma.Config.get([:ldap, :enabled]) - - on_exit(fn -> - Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, ldap_authenticator) - Pleroma.Config.put([:ldap, :enabled], ldap_enabled) - end) - - Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) + clear_config_all([:ldap, :enabled]) do Pleroma.Config.put([:ldap, :enabled], true) + end - :ok + clear_config_all(Pleroma.Web.Auth.Authenticator) do + Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) end @tag @skip diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 92e156347..b492c7794 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -11,23 +11,15 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.Token - @oauth_config_path [:oauth2, :issue_new_refresh_token] @session_opts [ store: :cookie, key: "_test", signing_salt: "cooldude" ] + clear_config_all([:instance, :account_activation_required]) describe "in OAuth consumer mode, " do setup do - oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies] - oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path) - Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook)) - - on_exit(fn -> - Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies) - end) - [ app: insert(:oauth_app), conn: @@ -37,6 +29,13 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do ] end + clear_config([:auth, :oauth_consumer_strategies]) do + Pleroma.Config.put( + [:auth, :oauth_consumer_strategies], + ~w(twitter facebook) + ) + end + test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ app: app, conn: conn @@ -775,12 +774,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do end test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do - setting = Pleroma.Config.get([:instance, :account_activation_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) - end + Pleroma.Config.put([:instance, :account_activation_required], true) password = "testpassword" user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) @@ -857,16 +851,10 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do end describe "POST /oauth/token - refresh token" do - setup do - oauth_token_config = Pleroma.Config.get(@oauth_config_path) - - on_exit(fn -> - Pleroma.Config.get(@oauth_config_path, oauth_token_config) - end) - end + clear_config([:oauth2, :issue_new_refresh_token]) test "issues a new access token with keep fresh token" do - Pleroma.Config.put(@oauth_config_path, true) + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true) user = insert(:user) app = insert(:oauth_app, scopes: ["read", "write"]) @@ -906,7 +894,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do end test "issues a new access token with new fresh token" do - Pleroma.Config.put(@oauth_config_path, false) + Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false) user = insert(:user) app = insert(:oauth_app, scopes: ["read", "write"]) diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 9f756effb..095ae7041 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -15,16 +15,13 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - :ok end + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) + end + describe "salmon_incoming" do test "decodes a salmon", %{conn: conn} do user = insert(:user) diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs new file mode 100644 index 000000000..ed6b79727 --- /dev/null +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -0,0 +1,94 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "/api/v1/pleroma/conversations/:id", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"}) + + [participation] = Participation.for_user(other_user) + + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/conversations/#{participation.id}") + |> json_response(200) + + assert result["id"] == participation.id |> to_string() + end + + test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{third_user.nickname}!", "visibility" => "direct"}) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"}) + + [participation] = Participation.for_user(other_user) + + {:ok, activity_two} = + CommonAPI.post(other_user, %{ + "status" => "Hi!", + "in_reply_to_status_id" => activity.id, + "in_reply_to_conversation_id" => participation.id + }) + + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") + |> json_response(200) + + assert length(result) == 2 + + id_one = activity.id + id_two = activity_two.id + assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result + end + + test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + + participation = Repo.preload(participation, :recipients) + + assert [user] == participation.recipients + assert other_user not in participation.recipients + + result = + conn + |> assign(:user, user) + |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{ + "recipients" => [user.id, other_user.id] + }) + |> json_response(200) + + assert result["id"] == participation.id |> to_string + + [participation] = Participation.for_user(user) + participation = Repo.preload(participation, :recipients) + + assert user in participation.recipients + assert other_user in participation.recipients + end +end diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs index c01e01124..bb2e1687a 100644 --- a/test/web/plugs/federating_plug_test.exs +++ b/test/web/plugs/federating_plug_test.exs @@ -4,15 +4,7 @@ defmodule Pleroma.Web.FederatingPlugTest do use Pleroma.Web.ConnCase - - setup_all do - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - - :ok - end + clear_config_all([:instance, :federating]) test "returns and halt the conn when federating is disabled" do Pleroma.Config.put([:instance, :federating], false) diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs index 85515c432..2251fed16 100644 --- a/test/web/rel_me_test.exs +++ b/test/web/rel_me_test.exs @@ -5,33 +5,8 @@ defmodule Pleroma.Web.RelMeTest do use ExUnit.Case, async: true - setup do - Tesla.Mock.mock(fn - %{ - method: :get, - url: "http://example.com/rel_me/anchor" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/anchor_nofollow" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/link" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")} - - %{ - method: :get, - url: "http://example.com/rel_me/null" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")} - end) - + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end diff --git a/test/web/rich_media/aws_signed_url_test.exs b/test/web/rich_media/aws_signed_url_test.exs index 122787bc2..a3a50cbb1 100644 --- a/test/web/rich_media/aws_signed_url_test.exs +++ b/test/web/rich_media/aws_signed_url_test.exs @@ -60,7 +60,8 @@ defmodule Pleroma.Web.RichMedia.TTL.AwsSignedUrlTest do {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) # as there is delay in setting and pulling the data from cache we ignore 1 second - assert_in_delta(valid_till * 1000, cache_ttl, 1000) + # make it 2 seconds for flakyness + assert_in_delta(valid_till * 1000, cache_ttl, 2000) end defp construct_s3_url(timestamp, valid_till) do diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs index 92198f3d9..48884319d 100644 --- a/test/web/rich_media/helpers_test.exs +++ b/test/web/rich_media/helpers_test.exs @@ -15,12 +15,12 @@ defmodule Pleroma.Web.RichMedia.HelpersTest do setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - rich_media = Config.get([:rich_media, :enabled]) - on_exit(fn -> Config.put([:rich_media, :enabled], rich_media) end) :ok end + clear_config([:rich_media, :enabled]) + test "refuses to crawl incomplete URLs" do user = insert(:user) diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs index d47b37efb..96fa7645f 100644 --- a/test/web/streamer_test.exs +++ b/test/web/streamer_test.exs @@ -11,15 +11,7 @@ defmodule Pleroma.Web.StreamerTest do alias Pleroma.Web.Streamer import Pleroma.Factory - setup do - skip_thread_containment = Pleroma.Config.get([:instance, :skip_thread_containment]) - - on_exit(fn -> - Pleroma.Config.put([:instance, :skip_thread_containment], skip_thread_containment) - end) - - :ok - end + clear_config_all([:instance, :skip_thread_containment]) describe "user streams" do setup do @@ -414,6 +406,26 @@ defmodule Pleroma.Web.StreamerTest do Task.await(task) end + test "it doesn't send posts from muted threads" do + user = insert(:user) + user2 = insert(:user) + {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + + {:ok, activity} = CommonAPI.add_mute(user2, activity) + + task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) + + Streamer.add_socket( + "user", + %{transport_pid: task.pid, assigns: %{user: user2}} + ) + + Streamer.stream("user", activity) + Task.await(task) + end + describe "direct streams" do setup do GenServer.start(Streamer, %{}, name: Streamer) diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 8bb8aa36d..8ef14b4c5 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -151,6 +151,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do describe "GET /statuses/public_timeline.json" do setup [:valid_user] + clear_config([:instance, :public]) test "returns statuses", %{conn: conn} do user = insert(:user) @@ -173,8 +174,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do conn |> get("/api/statuses/public_timeline.json") |> json_response(403) - - Pleroma.Config.put([:instance, :public], true) end test "returns 200 to authenticated request when the instance is not public", @@ -185,8 +184,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(user.nickname, "test") |> get("/api/statuses/public_timeline.json") |> json_response(200) - - Pleroma.Config.put([:instance, :public], true) end test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do @@ -220,6 +217,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do describe "GET /statuses/public_and_external_timeline.json" do setup [:valid_user] + clear_config([:instance, :public]) test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do Pleroma.Config.put([:instance, :public], false) @@ -227,8 +225,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do conn |> get("/api/statuses/public_and_external_timeline.json") |> json_response(403) - - Pleroma.Config.put([:instance, :public], true) end test "returns 200 to authenticated request when the instance is not public", @@ -239,8 +235,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(user.nickname, "test") |> get("/api/statuses/public_and_external_timeline.json") |> json_response(200) - - Pleroma.Config.put([:instance, :public], true) end test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do @@ -1176,13 +1170,6 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do describe "POST /api/account/resend_confirmation_email" do setup do - setting = Pleroma.Config.get([:instance, :account_activation_required]) - - unless setting do - Pleroma.Config.put([:instance, :account_activation_required], true) - on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) - end - user = insert(:user) info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) @@ -1197,6 +1184,10 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do [user: user] end + clear_config([:instance, :account_activation_required]) do + Pleroma.Config.put([:instance, :account_activation_required], true) + end + test "it returns 204 No Content", %{conn: conn, user: user} do conn |> assign(:user, user) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 640579c09..fe4ffdb59 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -14,20 +14,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do setup do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - instance_config = Pleroma.Config.get([:instance]) - pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) - deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) - - on_exit(fn -> - Pleroma.Config.put([:instance], instance_config) - Pleroma.Config.put([:frontend_configurations, :pleroma_fe], pleroma_fe) - Pleroma.Config.put([:user, :deny_follow_blocked], deny_follow_blocked) - end) - :ok end + clear_config([:instance]) + clear_config([:frontend_configurations, :pleroma_fe]) + clear_config([:user, :deny_follow_blocked]) + describe "POST /api/pleroma/follow_import" do test "it returns HTTP 200", %{conn: conn} do user1 = insert(:user) @@ -260,7 +253,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end test "returns the state of safe_dm_mentions flag", %{conn: conn} do - option = Pleroma.Config.get([:instance, :safe_dm_mentions]) Pleroma.Config.put([:instance, :safe_dm_mentions], true) response = @@ -278,8 +270,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do |> json_response(:ok) assert response["site"]["safeDMMentionsEnabled"] == "0" - - Pleroma.Config.put([:instance, :safe_dm_mentions], option) end test "it returns the managed config", %{conn: conn} do @@ -534,15 +524,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end describe "GET /api/pleroma/healthcheck" do - setup do - config_healthcheck = Pleroma.Config.get([:instance, :healthcheck]) - - on_exit(fn -> - Pleroma.Config.put([:instance, :healthcheck], config_healthcheck) - end) - - :ok - end + clear_config([:instance, :healthcheck]) test "returns 503 when healthcheck disabled", %{conn: conn} do Pleroma.Config.put([:instance, :healthcheck], false) diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs index 7d861cbf5..e23086b2a 100644 --- a/test/web/web_finger/web_finger_controller_test.exs +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -10,15 +10,13 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) :ok end + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) + end + test "GET host-meta" do response = build_conn() diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs index aa7262beb..59cacbe68 100644 --- a/test/web/websub/websub_controller_test.exs +++ b/test/web/websub/websub_controller_test.exs @@ -9,14 +9,8 @@ defmodule Pleroma.Web.Websub.WebsubControllerTest do alias Pleroma.Web.Websub alias Pleroma.Web.Websub.WebsubClientSubscription - setup_all do - config_path = [:instance, :federating] - initial_setting = Pleroma.Config.get(config_path) - - Pleroma.Config.put(config_path, true) - on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) - - :ok + clear_config_all([:instance, :federating]) do + Pleroma.Config.put([:instance, :federating], true) end test "websub subscription request", %{conn: conn} do