Merge remote-tracking branch 'remotes/origin/develop' into 1427-oauth-admin-scopes
authorIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 5 Dec 2019 21:26:31 +0000 (00:26 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Thu, 5 Dec 2019 21:26:31 +0000 (00:26 +0300)
75 files changed:
.gitlab-ci.yml
CHANGELOG.md
benchmarks/load_testing/fetcher.ex
benchmarks/load_testing/generator.ex
benchmarks/mix/tasks/pleroma/load_testing.ex
config/config.exs
config/prod.exs
config/releases.exs
config/test.exs
docs/API/admin_api.md
docs/API/differences_in_mastoapi_responses.md
docs/administration/CLI_tasks/user.md
docs/configuration/cheatsheet.md
lib/mix/pleroma.ex
lib/mix/tasks/pleroma/user.ex
lib/pleroma/activity.ex
lib/pleroma/application.ex
lib/pleroma/following_relationship.ex
lib/pleroma/moderation_log.ex
lib/pleroma/notification.ex
lib/pleroma/object.ex
lib/pleroma/object/fetcher.ex
lib/pleroma/user.ex
lib/pleroma/user/search.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/relay.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/activity_pub/visibility.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/notification_view.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/push/impl.ex
lib/pleroma/web/router.ex
lib/pleroma/workers/background_worker.ex
mix.exs
mix.lock
priv/repo/migrations/20191025081729_add_move_support_to_users.exs [new file with mode: 0644]
priv/repo/migrations/20191104133100_set_visible_service_actors.exs [new file with mode: 0644]
priv/repo/migrations/20191123103423_remove_info_from_users.exs [new file with mode: 0644]
priv/repo/migrations/20191128153944_fix_missing_following_count.exs [new file with mode: 0644]
priv/static/schemas/litepub-0.1.jsonld
rel/env.sh.eex
test/federation/federation_test.exs [new file with mode: 0644]
test/fixtures/tesla_mock/admin@mastdon.example.org.json
test/fixtures/users_mock/friendica_followers.json [new file with mode: 0644]
test/fixtures/users_mock/friendica_following.json [new file with mode: 0644]
test/following_relationship_test.exs [new file with mode: 0644]
test/html_test.exs
test/http/request_builder_test.exs
test/notification_test.exs
test/support/cluster.ex [new file with mode: 0644]
test/support/factory.ex
test/support/http_request_mock.ex
test/test_helper.exs
test/user_search_test.exs
test/user_test.exs
test/web/activity_pub/activity_pub_controller_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/mastodon_api/controllers/filter_controller_test.exs
test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/mastodon_api/views/notification_view_test.exs
test/web/ostatus/ostatus_controller_test.exs
test/web/streamer/streamer_test.exs

index ab62c88275958f118e6783567864eb5daf1e8c78..88789035d43952032792e453a5d9d3238ed382af 100644 (file)
@@ -31,6 +31,7 @@ build:
 
 benchmark:
   stage: benchmark
+  when: manual
   variables:
     MIX_ENV: benchmark
   services:
@@ -55,6 +56,19 @@ unit-testing:
     - mix ecto.migrate
     - mix coveralls --preload-modules
 
+federated-testing:
+  stage: test
+  services:
+  - name: minibikini/postgres-with-rum:12
+    alias: postgres
+    command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
+  script:
+    - mix deps.get
+    - mix ecto.create
+    - mix ecto.migrate
+    - epmd -daemon
+    - mix test --trace --only federated
+
 unit-testing-rum:
   stage: test
   services:
index dac61c1749d2238721cea5218db7a626493531f8..007c6f114294ddaaac45f8b1372b47f73f85b070 100644 (file)
@@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Authentication: Added rate limit for password-authorized actions / login existence checks
 - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
 - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
+- Mix task to list all users (`mix pleroma.user list`)
 - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
 - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
 - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
@@ -66,10 +67,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
   - `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body)
 - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
 - Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
+- ActivityPub: Support `Move` activities
 - Mastodon API: Add `/api/v1/markers` for managing timeline read markers
 - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
 - Configuration: `feed` option for user atom feed.
 - Pleroma API: Add Emoji reactions
+- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
+- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
 </details>
 
 ### Fixed
@@ -80,6 +84,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
 - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
+- Admin API: Error when trying to update reports in the "old" format
 </details>
 
 ## [1.1.6] - 2019-11-19
index cdc073b2ebea0f64f704c44a620f0f3e67c4398a..a45a71d4a4b706fc80e6fd0bcc2a5e0c0b6a482d 100644 (file)
@@ -95,7 +95,36 @@ defmodule Pleroma.LoadTesting.Fetcher do
           for: user,
           as: :activity
         })
-      end
+      end,
+      "Rendering favorites timeline" => fn ->
+        conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil)
+        Pleroma.Web.MastodonAPI.StatusController.favourites(
+          %Plug.Conn{conn |
+                     assigns: %{user: user},
+                     query_params:  %{"limit" => "0"},
+                     body_params: %{},
+                     cookies: %{},
+                     params: %{},
+                     path_params: %{},
+                     private: %{
+                       Pleroma.Web.Router => {[], %{}},
+                       phoenix_router: Pleroma.Web.Router,
+                       phoenix_action: :favourites,
+                       phoenix_controller: Pleroma.Web.MastodonAPI.StatusController,
+                       phoenix_endpoint: Pleroma.Web.Endpoint,
+                       phoenix_format: "json",
+                       phoenix_layout: {Pleroma.Web.LayoutView, "app.html"},
+                       phoenix_recycled: true,
+
+                       phoenix_view: Pleroma.Web.MastodonAPI.StatusView,
+                       plug_session: %{"user_id" => user.id},
+                       plug_session_fetch: :done,
+                       plug_session_info: :write,
+                       plug_skip_csrf_protection: true
+                     }
+          },
+          %{})
+      end,
     })
   end
 
index b4432bdb7313621dc92c5d97583cdc00082864c7..a957e0ffb47d84c5f860d336fe9d5bb5f44109bc 100644 (file)
@@ -2,6 +2,24 @@ defmodule Pleroma.LoadTesting.Generator do
   use Pleroma.LoadTesting.Helper
   alias Pleroma.Web.CommonAPI
 
+  def generate_like_activities(user, posts) do
+    count_likes = Kernel.trunc(length(posts) / 4)
+    IO.puts("Starting generating #{count_likes} like activities...")
+
+    {time, _} =
+      :timer.tc(fn ->
+        Task.async_stream(
+           Enum.take_random(posts, count_likes),
+          fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end,
+          max_concurrency: 10,
+          timeout: 30_000
+        )
+        |> Stream.run()
+      end)
+
+    IO.puts("Inserting like activities take #{to_sec(time)} sec.\n")
+  end
+
   def generate_users(opts) do
     IO.puts("Starting generating #{opts[:users_max]} users...")
     {time, _} = :timer.tc(fn -> do_generate_users(opts) end)
@@ -31,7 +49,6 @@ defmodule Pleroma.LoadTesting.Generator do
       password_hash:
         "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
       bio: "Tester Number #{i}",
-      info: %{},
       local: remote
     }
 
index 4fa3eec493014d8f4dbf7fb2676a05f120785f50..0a751adac02ab3a9088382ca6f8d7c899208ae50 100644 (file)
@@ -100,6 +100,10 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do
 
     generate_remote_activities(user, remote_users)
 
+    generate_like_activities(
+      user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create"))
+    )
+
     generate_dms(user, users, opts)
 
     {:ok, activity} = generate_long_thread(user, users, opts)
index 64397484ef80f50c5a19cdccce974d87eb14fc6d..a8534da5417ca042eefb815985bfa5cfd1c320c9 100644 (file)
@@ -180,7 +180,8 @@ config :pleroma, Pleroma.Web.Endpoint,
 
 # Configures Elixir's Logger
 config :logger, :console,
-  format: "$time $metadata[$level] $message\n",
+  level: :debug,
+  format: "\n$time $metadata[$level] $message\n",
   metadata: [:request_id]
 
 config :logger, :ex_syslogger,
@@ -208,6 +209,7 @@ config :tesla, adapter: Tesla.Adapter.Hackney
 config :pleroma, :http,
   proxy_url: nil,
   send_user_agent: true,
+  user_agent: :default,
   adapter: [
     ssl_options: [
       # Workaround for remote server certificate chain issues
index 9c205cbd2f02cde66445e94235a807413dcf70e0..25873f360eabbebced42d3f3af5e08530d0944a9 100644 (file)
@@ -20,7 +20,8 @@ config :pleroma, Pleroma.Web.Endpoint,
 config :phoenix, serve_endpoints: true
 
 # Do not print debug messages in production
-config :logger, level: :warn
+config :logger, :console, level: :warn
+config :logger, :ex_syslogger, level: :warn
 
 # ## SSL Support
 #
index 36c49367384be2eb63b9089ffdfbfd6ae1f1b278..98c5ceccd54a5501dfefcd8a845d148aedca390f 100644 (file)
@@ -1,6 +1,6 @@
 import Config
 
-config :pleroma, :instance, static: "/var/lib/pleroma/static"
+config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
 config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
 
 config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
index da2778aa749ef6b32052f0bce6ae06f91b72eb47..9b737d4d7e3960588d74819f8e2fa423d3077d72 100644 (file)
@@ -15,7 +15,9 @@ config :pleroma, Pleroma.Captcha,
   method: Pleroma.Captcha.Mock
 
 # Print only warnings and errors during test
-config :logger, level: :warn
+config :logger, :console,
+  level: :warn,
+  format: "\n[$level] $message\n"
 
 config :pleroma, :auth, oauth_consumer_strategies: []
 
index f64983a9041a9e479637737a4533e509f09a0331..2cac317def2df0790ae1ffd04db05ec0fd455c66 100644 (file)
@@ -870,3 +870,19 @@ Compile time settings (need instance reboot):
 - Authentication: required
 - Params: None
 - Response: JSON, "ok" and 200 status
+
+## `PATCH /api/pleroma/admin/users/confirm_email`
+
+### Confirm users' emails
+
+- Params:
+  - `nicknames`
+- Response: Array of user nicknames
+
+## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
+
+### Resend confirmation email
+
+- Params:
+  - `nicknames`
+- Response: Array of user nicknames
index 7fbe171306f54e41369bea8dc3b9184fcca2d903..006d17c1bc28abd63ae6331f0cf9e9ab99dbac9a 100644 (file)
@@ -57,6 +57,7 @@ Has these additional fields under the `pleroma` object:
 - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
 - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
 - `deactivated`: boolean, true when the user is deactivated
+- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
 - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
 
 ### Source
@@ -91,6 +92,12 @@ Has these additional fields under the `pleroma` object:
 
 - `is_seen`: true if the notification was read by the user
 
+### Move Notification
+
+The `type` value is `move`. Has an additional field:
+
+- `target`: new account
+
 ## GET `/api/v1/notifications`
 
 Accepts additional parameters:
@@ -136,6 +143,7 @@ Additional parameters can be added to the JSON body/Form data:
 - `default_scope` - the scope returned under `privacy` key in Source subentity
 - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
 - `skip_thread_containment` - if true, skip filtering out broken threads
+- `allow_following_move` - if true, allows automatically follow moved following accounts
 - `pleroma_background_image` - sets the background image of the user.
 
 ### Pleroma Settings Store
index cf120f2c9fb91fc72f727ab2fb9183a2c25cf8b4..96b2d9e6a3326fafcd5bf54e0cceeaa88a288bf0 100644 (file)
@@ -15,6 +15,11 @@ $PREFIX new <nickname> <email> [<options>]
 - `--admin`/`--no-admin` - whether the user should be an admin
 - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
 
+## List local users
+```sh
+$PREFIX list
+```
+
 ## Generate an invite link
 ```sh
 $PREFIX invite [<options>]
index 07d9a1d451765274f18633ff821fc31f4efab4c9..dc2f5522990d7a27dcab18d4f8d9ea1eaf6e52bf 100644 (file)
@@ -348,7 +348,17 @@ Available caches:
 * `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
 * `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
 
-## :hackney_pools
+## HTTP client
+
+### :http
+
+* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
+* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
+* `user_agent`: what user agent should  we use? (default: `:default`), must be string or `:default`
+* `adapter`: array of hackney options
+
+
+### :hackney_pools
 
 Advanced. Tweaks Hackney (http client) connections pools.
 
index faeb30e1dc378da65b601ce01f4deb537056a2e6..73a076a531a6ee7fc9963488f6a6ff5804029340 100644 (file)
@@ -6,6 +6,11 @@ defmodule Mix.Pleroma do
   @doc "Common functions to be reused in mix tasks"
   def start_pleroma do
     Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
+
+    if Pleroma.Config.get(:env) != :test do
+      Application.put_env(:logger, :console, level: :debug)
+    end
+
     {:ok, _} = Application.ensure_all_started(:pleroma)
   end
 
index 8c47392214b29a4f09c8120085062cdfcdf5eb1c..eff5191f5397d98f3b99ec6f58749c1368549d73 100644 (file)
@@ -362,6 +362,24 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
+  def run(["list"]) do
+    start_pleroma()
+
+    Pleroma.User.Query.build(%{local: true})
+    |> Pleroma.RepoStreamer.chunk_stream(500)
+    |> Stream.each(fn users ->
+      users
+      |> Enum.each(fn user ->
+        shell_info(
+          "#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{
+            user.info.locked
+          }, deactivated: #{user.info.deactivated}"
+        )
+      end)
+    end)
+    |> Stream.run()
+  end
+
   defp set_moderator(user, value) do
     {:ok, user} =
       user
index 7e283df32c01aeae7ac4eb981a670aef66e09da0..f180c1e3311021a92ea4e2bd40dd53e877d2fa6c 100644 (file)
@@ -28,7 +28,8 @@ defmodule Pleroma.Activity do
     "Create" => "mention",
     "Follow" => "follow",
     "Announce" => "reblog",
-    "Like" => "favourite"
+    "Like" => "favourite",
+    "Move" => "move"
   }
 
   @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
@@ -303,4 +304,17 @@ defmodule Pleroma.Activity do
   end
 
   defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
+
+  def direct_conversation_id(activity, for_user) do
+    alias Pleroma.Conversation.Participation
+
+    with %{data: %{"context" => context}} when is_binary(context) <- activity,
+         %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
+         %Participation{id: participation_id} <-
+           Participation.for_user_and_conversation(for_user, conversation) do
+      participation_id
+    else
+      _ -> nil
+    end
+  end
 end
index 2b6a55f98fda23c120db6e170374a633fa92b1c3..9dbd1e26b699130c8b507d794de8191bcef530f7 100644 (file)
@@ -17,8 +17,14 @@ defmodule Pleroma.Application do
   def repository, do: @repository
 
   def user_agent do
-    info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
-    named_version() <> "; " <> info
+    case Pleroma.Config.get([:http, :user_agent], :default) do
+      :default ->
+        info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
+        named_version() <> "; " <> info
+
+      custom ->
+        custom
+    end
   end
 
   # See http://elixir-lang.org/docs/stable/elixir/Application.html
index 2ffac17ee135667774c0cd64f110d6425fc81706..a03c9bd304c595bb6e16832866001328c4951a2f 100644 (file)
@@ -101,10 +101,28 @@ defmodule Pleroma.FollowingRelationship do
       |> select([r, u], u.follower_address)
       |> Repo.all()
 
-    if not user.local or user.nickname in [nil, "internal.fetch"] do
+    if not user.local or user.invisible do
       following
     else
       [user.follower_address | following]
     end
   end
+
+  def move_following(origin, target) do
+    __MODULE__
+    |> join(:inner, [r], f in assoc(r, :follower))
+    |> where(following_id: ^origin.id)
+    |> where([r, f], f.allow_following_move == true)
+    |> limit(50)
+    |> preload([:follower])
+    |> Repo.all()
+    |> Enum.map(fn following_relationship ->
+      Repo.delete(following_relationship)
+      Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
+    end)
+    |> case do
+      [] -> :ok
+      _ -> move_following(origin, target)
+    end
+  end
 end
index ffa5dc25db48a9bfa576a32d8658cf4f46169e27..706f089dc044f02c07d10ac2b621db30629bbf81 100644 (file)
@@ -624,7 +624,31 @@ defmodule Pleroma.ModerationLog do
           "subject" => subjects
         }
       }) do
-    "@#{actor_nickname} force password reset for users: #{users_to_nicknames_string(subjects)}"
+    "@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
+  end
+
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "confirm_email",
+          "subject" => subjects
+        }
+      }) do
+    "@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
+  end
+
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "resend_confirmation_email",
+          "subject" => subjects
+        }
+      }) do
+    "@#{actor_nickname} re-sent confirmation email for users: #{
+      users_to_nicknames_string(subjects)
+    }"
   end
 
   defp nicknames_to_string(nicknames) do
index b7ecf51e4681fc9a25b8d1cd9d596c928f930bba..f37e7ec671ee88c39848cc92a7f715621766999e 100644 (file)
@@ -251,10 +251,13 @@ defmodule Pleroma.Notification do
     end
   end
 
-  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
-      when type in ["Like", "Announce", "Follow"] do
-    users = get_notified_from_activity(activity)
-    notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
+  def create_notifications(%Activity{data: %{"type" => type}} = activity)
+      when type in ["Like", "Announce", "Follow", "Move"] do
+    notifications =
+      activity
+      |> get_notified_from_activity()
+      |> Enum.map(&create_notification(activity, &1))
+
     {:ok, notifications}
   end
 
@@ -276,19 +279,15 @@ defmodule Pleroma.Notification do
 
   def get_notified_from_activity(activity, local_only \\ true)
 
-  def get_notified_from_activity(
-        %Activity{data: %{"to" => _, "type" => type} = _data} = activity,
-        local_only
-      )
-      when type in ["Create", "Like", "Announce", "Follow"] do
-    recipients =
-      []
-      |> Utils.maybe_notify_to_recipients(activity)
-      |> Utils.maybe_notify_mentioned_recipients(activity)
-      |> Utils.maybe_notify_subscribers(activity)
-      |> Enum.uniq()
-
-    User.get_users_from_set(recipients, local_only)
+  def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
+      when type in ["Create", "Like", "Announce", "Follow", "Move"] do
+    []
+    |> Utils.maybe_notify_to_recipients(activity)
+    |> Utils.maybe_notify_mentioned_recipients(activity)
+    |> Utils.maybe_notify_subscribers(activity)
+    |> Utils.maybe_notify_followers(activity)
+    |> Enum.uniq()
+    |> User.get_users_from_set(local_only)
   end
 
   def get_notified_from_activity(_, _local_only), do: []
index d9b41d710cebb6296a3b2d4cb41521feb2c27030..b4ed3a9b2d1ba7b0004e213f0924009cb081bc8c 100644 (file)
@@ -63,7 +63,7 @@ defmodule Pleroma.Object do
   end
 
   defp warn_on_no_object_preloaded(ap_id) do
-    "Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object"
+    "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
     |> Logger.debug()
 
     Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
@@ -255,4 +255,8 @@ defmodule Pleroma.Object do
     |> Object.change(%{data: Map.merge(data || %{}, attrs)})
     |> Repo.update()
   end
+
+  def local?(%Object{data: %{"id" => id}}) do
+    String.starts_with?(id, Pleroma.Web.base_url() <> "/")
+  end
 end
index 9a9a4655005d5ab8dcb21511e11ebda5ca7d73a7..4d71c91a80be38f5470690048495c3d4f8ba227c 100644 (file)
@@ -49,7 +49,7 @@ defmodule Pleroma.Object.Fetcher do
   end
 
   def refetch_object(%Object{data: %{"id" => id}} = object) do
-    with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
+    with {:local, false} <- {:local, Object.local?(object)},
          {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
          {:ok, object} <- reinject_object(object, data) do
       {:ok, object}
index d05dccd3da4311538653093e49babd60972610a7..7b8222ce170db28c093617640c9da6226ba2363f 100644 (file)
@@ -67,8 +67,7 @@ defmodule Pleroma.User do
     field(:source_data, :map, default: %{})
     field(:note_count, :integer, default: 0)
     field(:follower_count, :integer, default: 0)
-    # Should be filled in only for remote users
-    field(:following_count, :integer, default: nil)
+    field(:following_count, :integer, default: 0)
     field(:locked, :boolean, default: false)
     field(:confirmation_pending, :boolean, default: false)
     field(:password_reset_pending, :boolean, default: false)
@@ -104,7 +103,9 @@ defmodule Pleroma.User do
     field(:raw_fields, {:array, :map}, default: [])
     field(:discoverable, :boolean, default: false)
     field(:invisible, :boolean, default: false)
+    field(:allow_following_move, :boolean, default: true)
     field(:skip_thread_containment, :boolean, default: false)
+    field(:also_known_as, {:array, :string}, default: [])
 
     field(:notification_settings, :map,
       default: %{
@@ -119,8 +120,6 @@ defmodule Pleroma.User do
     has_many(:registrations, Registration)
     has_many(:deliveries, Delivery)
 
-    field(:info, :map, default: %{})
-
     timestamps()
   end
 
@@ -134,6 +133,8 @@ defmodule Pleroma.User do
 
   def visible_for?(user, for_user \\ nil)
 
+  def visible_for?(%User{invisible: true}, _), do: false
+
   def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
 
   def visible_for?(%User{} = user, for_user) do
@@ -176,22 +177,6 @@ defmodule Pleroma.User do
   def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
   def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 
-  def user_info(%User{} = user, args \\ %{}) do
-    following_count =
-      Map.get(args, :following_count, user.following_count || following_count(user))
-
-    follower_count = Map.get(args, :follower_count, user.follower_count)
-
-    %{
-      note_count: user.note_count,
-      locked: user.locked,
-      confirmation_pending: user.confirmation_pending,
-      default_scope: user.default_scope
-    }
-    |> Map.put(:following_count, following_count)
-    |> Map.put(:follower_count, follower_count)
-  end
-
   def follow_state(%User{} = user, %User{} = target) do
     case Utils.fetch_latest_follow(user, target) do
       %{data: %{"state" => state}} -> state
@@ -210,10 +195,6 @@ defmodule Pleroma.User do
     Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
   end
 
-  def set_info_cache(user, args) do
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
-  end
-
   @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
   def restrict_deactivated(query) do
     from(u in query, where: u.deactivated != ^true)
@@ -244,7 +225,6 @@ defmodule Pleroma.User do
 
     params =
       params
-      |> Map.put(:info, params[:info] || %{})
       |> truncate_if_exists(:name, name_limit)
       |> truncate_if_exists(:bio, bio_limit)
       |> truncate_fields_param()
@@ -273,7 +253,8 @@ defmodule Pleroma.User do
           :fields,
           :following_count,
           :discoverable,
-          :invisible
+          :invisible,
+          :also_known_as
         ]
       )
       |> validate_required([:name, :ap_id])
@@ -315,13 +296,15 @@ defmodule Pleroma.User do
         :hide_followers_count,
         :hide_follows_count,
         :hide_favorites,
+        :allow_following_move,
         :background,
         :show_role,
         :skip_thread_containment,
         :fields,
         :raw_fields,
         :pleroma_settings_store,
-        :discoverable
+        :discoverable,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
@@ -359,9 +342,11 @@ defmodule Pleroma.User do
         :hide_follows,
         :fields,
         :hide_followers,
+        :allow_following_move,
         :discoverable,
         :hide_followers_count,
-        :hide_follows_count
+        :hide_follows_count,
+        :also_known_as
       ]
     )
     |> unique_constraint(:nickname)
@@ -492,6 +477,10 @@ defmodule Pleroma.User do
     end
   end
 
+  def try_send_confirmation_email(users) do
+    Enum.each(users, &try_send_confirmation_email/1)
+  end
+
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -522,14 +511,9 @@ defmodule Pleroma.User do
   @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
   @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
   def follow_all(follower, followeds) do
-    followeds =
-      Enum.reject(followeds, fn followed ->
-        blocks?(follower, followed) || blocks?(followed, follower)
-      end)
-
-    Enum.each(followeds, &follow(follower, &1, "accept"))
-
-    Enum.each(followeds, &update_follower_count/1)
+    followeds
+    |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+    |> Enum.each(&follow(follower, &1, "accept"))
 
     set_cache(follower)
   end
@@ -549,11 +533,11 @@ defmodule Pleroma.User do
       true ->
         FollowingRelationship.follow(follower, followed, state)
 
-        follower = maybe_update_following_count(follower)
-
         {:ok, _} = update_follower_count(followed)
 
-        set_cache(follower)
+        follower
+        |> update_following_count()
+        |> set_cache()
     end
   end
 
@@ -561,11 +545,12 @@ defmodule Pleroma.User do
     if following?(follower, followed) and follower.ap_id != followed.ap_id do
       FollowingRelationship.unfollow(follower, followed)
 
-      follower = maybe_update_following_count(follower)
-
       {:ok, followed} = update_follower_count(followed)
 
-      set_cache(follower)
+      {:ok, follower} =
+        follower
+        |> update_following_count()
+        |> set_cache()
 
       {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
     else
@@ -615,7 +600,6 @@ defmodule Pleroma.User do
   def set_cache(%User{} = user) do
     Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
     Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
     {:ok, user}
   end
 
@@ -634,7 +618,6 @@ defmodule Pleroma.User do
   def invalidate_cache(user) do
     Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
     Cachex.del(:user_cache, "nickname:#{user.nickname}")
-    Cachex.del(:user_cache, "user_info:#{user.id}")
   end
 
   def get_cached_by_ap_id(ap_id) do
@@ -702,11 +685,6 @@ defmodule Pleroma.User do
     get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
   end
 
-  def get_cached_user_info(user) do
-    key = "user_info:#{user.id}"
-    Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
-  end
-
   def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
 
   def get_or_fetch_by_nickname(nickname) do
@@ -895,8 +873,8 @@ 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
+  @spec update_following_count(User.t()) :: User.t()
+  def update_following_count(%User{local: false} = user) do
     if Pleroma.Config.get([:instance, :external_user_synchronization]) do
       maybe_fetch_follow_information(user)
     else
@@ -904,7 +882,13 @@ defmodule Pleroma.User do
     end
   end
 
-  def maybe_update_following_count(user), do: user
+  def update_following_count(%User{local: true} = user) do
+    following_count = FollowingRelationship.following_count(user)
+
+    user
+    |> follow_information_changeset(%{following_count: following_count})
+    |> Repo.update!()
+  end
 
   def set_unread_conversation_count(%User{local: true} = user) do
     unread_query = Participation.unread_conversation_count_for_user(user)
@@ -1097,7 +1081,12 @@ defmodule Pleroma.User do
 
   def deactivate(%User{} = user, status) do
     with {:ok, user} <- set_activation_status(user, status) do
-      Enum.each(get_followers(user), &invalidate_cache/1)
+      user
+      |> get_followers()
+      |> Enum.filter(& &1.local)
+      |> Enum.each(fn follower ->
+        follower |> update_following_count() |> set_cache()
+      end)
 
       # Only update local user counts, remote will be update during the next pull.
       user
@@ -1226,7 +1215,7 @@ defmodule Pleroma.User do
   def external_users(opts \\ []) do
     query =
       external_users_query()
-      |> select([u], struct(u, [:id, :ap_id, :info]))
+      |> select([u], struct(u, [:id, :ap_id]))
 
     query =
       if opts[:max_id],
@@ -1317,22 +1306,23 @@ defmodule Pleroma.User do
     end
   end
 
-  @doc "Creates an internal service actor by URI if missing.  Optionally takes nickname for addressing."
+  @doc """
+  Creates an internal service actor by URI if missing.
+  Optionally takes nickname for addressing.
+  """
   def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
-    with %User{} = user <- get_cached_by_ap_id(uri) do
-      user
-    else
-      _ ->
-        {:ok, user} =
-          %User{}
-          |> cast(%{}, [:ap_id, :nickname, :local])
-          |> put_change(:ap_id, uri)
-          |> put_change(:nickname, nickname)
-          |> put_change(:local, true)
-          |> put_change(:follower_address, uri <> "/followers")
-          |> Repo.insert()
+    with user when is_nil(user) <- get_cached_by_ap_id(uri) do
+      {:ok, user} =
+        %User{
+          invisible: true,
+          local: true,
+          ap_id: uri,
+          nickname: nickname,
+          follower_address: uri <> "/followers"
+        }
+        |> Repo.insert()
 
-        user
+      user
     end
   end
 
@@ -1575,6 +1565,11 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
+  @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
+  def toggle_confirmation(users) do
+    Enum.map(users, &toggle_confirmation/1)
+  end
+
   def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
     mascot
   end
index 09664db76b7393a144c9d54972ba3a884eabb7bf..b1bb9d4da0aa65ad14c14a1fe131f88380ecdc19 100644 (file)
@@ -45,6 +45,7 @@ defmodule Pleroma.User.Search do
     for_user
     |> base_query(following)
     |> filter_blocked_user(for_user)
+    |> filter_invisible_users()
     |> filter_blocked_domains(for_user)
     |> fts_search(query_string)
     |> trigram_rank(query_string)
@@ -98,6 +99,10 @@ defmodule Pleroma.User.Search do
   defp base_query(_user, false), do: User
   defp base_query(user, true), do: User.get_followers_query(user)
 
+  defp filter_invisible_users(query) do
+    from(q in query, where: q.invisible == false)
+  end
+
   defp filter_blocked_user(query, %User{blocks: blocks})
        when length(blocks) > 0 do
     from(q in query, where: not (q.ap_id in ^blocks))
index d0c014e9dba96052e96e9719db35b33f7a9a924e..d6a425d8bd0f6628e4a1ec606402806c4fea89d3 100644 (file)
@@ -541,6 +541,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  def move(%User{} = origin, %User{} = target, local \\ true) do
+    params = %{
+      "type" => "Move",
+      "actor" => origin.ap_id,
+      "object" => origin.ap_id,
+      "target" => target.ap_id
+    }
+
+    with true <- origin.ap_id in target.also_known_as,
+         {:ok, activity} <- insert(params, local) do
+      maybe_federate(activity)
+
+      BackgroundWorker.enqueue("move_following", %{
+        "origin_id" => origin.id,
+        "target_id" => target.id
+      })
+
+      {:ok, activity}
+    else
+      false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
+      err -> err
+    end
+  end
+
   defp fetch_activities_for_context_query(context, opts) do
     public = [Pleroma.Constants.as_public()]
 
@@ -734,6 +758,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> Enum.reverse()
   end
 
+  def fetch_instance_activities(params) do
+    params =
+      params
+      |> Map.put("type", ["Create", "Announce"])
+      |> Map.put("instance", params["instance"])
+      |> Map.put("whole_db", true)
+
+    fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+    |> Enum.reverse()
+  end
+
   defp user_activities_recipients(%{"godmode" => true}) do
     []
   end
@@ -961,6 +996,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_muted_reblogs(query, _), do: query
 
+  defp restrict_instance(query, %{"instance" => instance}) do
+    users =
+      from(
+        u in User,
+        select: u.ap_id,
+        where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
+      )
+      |> Repo.all()
+
+    from(activity in query, where: activity.actor in ^users)
+  end
+
+  defp restrict_instance(query, _), do: query
+
   defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
 
   defp exclude_poll_votes(query, _) do
@@ -1041,6 +1090,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_reblogs(opts)
     |> restrict_pinned(opts)
     |> restrict_muted_reblogs(opts)
+    |> restrict_instance(opts)
     |> Activity.restrict_deactivated_users()
     |> exclude_poll_votes(opts)
     |> exclude_visibility(opts)
@@ -1145,7 +1195,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
       name: data["name"],
       follower_address: data["followers"],
       following_address: data["following"],
-      bio: data["summary"]
+      bio: data["summary"],
+      also_known_as: Map.get(data, "alsoKnownAs", [])
     }
 
     # nickname can be nil because of virtual actors
@@ -1207,13 +1258,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
-  defp collection_private(data) do
-    if is_map(data["first"]) and
-         data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
+  defp collection_private(%{"first" => first}) do
+    if is_map(first) and
+         first["type"] in ["CollectionPage", "OrderedCollectionPage"] do
       {:ok, false}
     else
       with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
-             Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
+             Fetcher.fetch_and_contain_remote_object_from_id(first) do
         {:ok, false}
       else
         {:error, {:ok, %{status: code}}} when code in [401, 403] ->
@@ -1228,6 +1279,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  defp collection_private(_data), do: {:ok, true}
+
   def user_data_from_user_object(data) do
     with {:ok, data} <- MRF.filter(data),
          {:ok, data} <- object_to_user_data(data) do
index b2cd965fe0db08bc7cf183928a251ace949df6d9..dec5da0d3aea6ea4572bcf6ab100d3136a398c86 100644 (file)
@@ -45,7 +45,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   end
 
   def user(conn, %{"nickname" => nickname}) do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
+    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
          {:ok, user} <- User.ensure_keys_present(user) do
       conn
       |> put_resp_content_type("application/activity+json")
@@ -53,6 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> render("user.json", %{user: user})
     else
       nil -> {:error, :not_found}
+      %{local: false} -> {:error, :not_found}
     end
   end
 
index fc2619680f8e00d96606e7918b1291d65519dbe5..99a804568ccf0830529d55d1c17e916fce146c5f 100644 (file)
@@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.Relay do
       relay_ap_id()
       |> User.get_or_create_service_actor_by_ap_id()
 
-    {:ok, actor} = User.set_invisible(actor, true)
     actor
   end
 
index 15612545bd376fd97daddb81d2dc11e489683332..ce95fb6babf84a0c7ac38359c7bae335f2dea5aa 100644 (file)
@@ -669,7 +669,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
       update_data =
         new_user_data
-        |> Map.take([:avatar, :banner, :bio, :name])
+        |> Map.take([:avatar, :banner, :bio, :name, :also_known_as])
         |> Map.put(:fields, fields)
         |> Map.put(:locked, locked)
         |> Map.put(:invisible, invisible)
@@ -857,6 +857,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
+  def handle_incoming(
+        %{
+          "type" => "Move",
+          "actor" => origin_actor,
+          "object" => origin_actor,
+          "target" => target_actor
+        },
+        _options
+      ) do
+    with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
+         {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
+         true <- origin_actor in target_user.also_known_as do
+      ActivityPub.move(origin_user, target_user, false)
+    else
+      _e -> :error
+    end
+  end
+
   def handle_incoming(_, _), do: :error
 
   @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
index c456623596b372c91084c06adde6ffb259592579..01aacbde3fbac90d68f08d12ed0497383d4ceb22 100644 (file)
@@ -903,7 +903,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do
 
   def strip_report_status_data(activity) do
     [actor | reported_activities] = activity.data["object"]
-    stripped_activities = Enum.map(reported_activities, & &1["id"])
+
+    stripped_activities =
+      Enum.map(reported_activities, fn
+        act when is_map(act) -> act["id"]
+        act when is_binary(act) -> act
+      end)
+
     new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
 
     {:ok, %{activity | data: new_data}}
index cd409749348678e01a6ad3f14fd912ab0dc381ad..e172f6d3f2b6196338900b6448e80e17083b9b83 100644 (file)
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
   @spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
   def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
   def is_public?(%Object{data: data}), do: is_public?(data)
+  def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
   def is_public?(%Activity{data: data}), do: is_public?(data)
   def is_public?(%{"directMessage" => true}), do: false
   def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
index f9ace00d7e3b1f481a5b429fe28ddc4ccc77eed6..34d14710764ecbfa3602e6426215ab9dba75e012 100644 (file)
@@ -229,6 +229,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     end
   end
 
+  def list_instance_statuses(conn, %{"instance" => instance} = params) do
+    {page, page_size} = page_params(params)
+
+    activities =
+      ActivityPub.fetch_instance_activities(%{
+        "instance" => instance,
+        "limit" => page_size,
+        "offset" => (page - 1) * page_size
+      })
+
+    conn
+    |> put_view(StatusView)
+    |> render("index.json", %{activities: activities, as: :activity})
+  end
+
   def list_user_statuses(conn, %{"nickname" => nickname} = params) do
     godmode = params["godmode"] == "true" || params["godmode"] == true
 
@@ -337,7 +352,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     }
 
     with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
-         {:ok, users, count} <- filter_relay_user(users, count),
+         {:ok, users, count} <- filter_service_users(users, count),
          do:
            conn
            |> json(
@@ -349,15 +364,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
            )
   end
 
-  defp filter_relay_user(users, count) do
-    filtered_users = Enum.reject(users, &relay_user?/1)
-    count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count
+  defp filter_service_users(users, count) do
+    filtered_users = Enum.reject(users, &service_user?/1)
+    count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
 
     {:ok, filtered_users, count}
   end
 
-  defp relay_user?(user) do
-    user.ap_id == Relay.relay_ap_id()
+  defp service_user?(user) do
+    String.match?(user.ap_id, ~r/.*\/relay$/) or
+      String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
   end
 
   @filters ~w(local external active deactivated is_admin is_moderator)
@@ -801,6 +817,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     conn |> json("ok")
   end
 
+  def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+    User.toggle_confirmation(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "confirm_email"
+    })
+
+    conn |> json("")
+  end
+
+  def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+    User.try_send_confirmation_email(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "resend_confirmation_email"
+    })
+
+    conn |> json("")
+  end
+
   def errors(conn, {:error, :not_found}) do
     conn
     |> put_status(:not_found)
index 6aa7257cead895b166043f343f10f26d278fbb9a..d9dba5c5181693044ea8df1bb9f70900d8bdca40 100644 (file)
@@ -36,7 +36,8 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
       "deactivated" => user.deactivated,
       "local" => user.local,
       "roles" => User.roles(user),
-      "tags" => user.tags || []
+      "tags" => user.tags || [],
+      "confirmation_pending" => user.confirmation_pending
     }
   end
 
index 88a5f434a671277c0a949d22ad4173f0c07b5f37..cbb64f8d24f1c3cd2b2d4e4d8e96791504247a57 100644 (file)
@@ -451,6 +451,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     recipients ++ to
   end
 
+  def maybe_notify_to_recipients(recipients, _), do: recipients
+
   def maybe_notify_mentioned_recipients(
         recipients,
         %Activity{data: %{"to" => _to, "type" => type} = data} = activity
@@ -502,6 +504,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do
 
   def maybe_notify_subscribers(recipients, _), do: recipients
 
+  def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
+    with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
+      user
+      |> User.get_followers()
+      |> Enum.map(& &1.ap_id)
+      |> Enum.concat(recipients)
+    end
+  end
+
+  def maybe_notify_followers(recipients, _), do: recipients
+
   def maybe_extract_mentions(%{"tag" => tag}) do
     tag
     |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
index 5b01b964b8a52fb85aa05a60de590df627a9f87a..a69423f60eda71e54005b70c058136cc7ad0b925 100644 (file)
@@ -152,6 +152,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
         :hide_favorites,
         :show_role,
         :skip_thread_containment,
+        :allow_following_move,
         :discoverable
       ]
       |> Enum.reduce(%{}, fn key, acc ->
@@ -238,7 +239,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   @doc "GET /api/v1/accounts/:id"
   def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
-         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
+         true <- User.visible_for?(user, for_user) do
       render(conn, "show.json", user: user, for: for_user)
     else
       _e -> render_error(conn, :not_found, "Can't find user")
index e30fed6102b7b16a491cfc0f6010df1570371025..ec720e472e20bd368c80cf79fa82922239bf5a50 100644 (file)
@@ -71,18 +71,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
     image = User.avatar_url(user) |> MediaProxy.url()
     header = User.banner_url(user) |> MediaProxy.url()
-    user_info = User.get_cached_user_info(user)
 
     following_count =
       if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
-        user_info.following_count
+        user.following_count || 0
       else
         0
       end
 
     followers_count =
       if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
-        user_info.follower_count
+        user.follower_count || 0
       else
         0
       end
@@ -144,7 +143,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
       # Pleroma extension
       pleroma: %{
-        confirmation_pending: user_info.confirmation_pending,
+        confirmation_pending: user.confirmation_pending,
         tags: user.tags,
         hide_followers_count: user.hide_followers_count,
         hide_follows_count: user.hide_follows_count,
@@ -157,12 +156,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
       }
     }
     |> maybe_put_role(user, opts[:for])
-    |> maybe_put_settings(user, opts[:for], user_info)
+    |> maybe_put_settings(user, opts[:for], opts)
     |> maybe_put_notification_settings(user, opts[:for])
     |> maybe_put_settings_store(user, opts[:for], opts)
     |> maybe_put_chat_token(user, opts[:for], opts)
     |> maybe_put_activation_status(user, opts[:for])
     |> maybe_put_follow_requests_count(user, opts[:for])
+    |> maybe_put_allow_following_move(user, opts[:for])
     |> maybe_put_unread_conversation_count(user, opts[:for])
   end
 
@@ -191,7 +191,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
          data,
          %User{id: user_id} = user,
          %User{id: user_id},
-         _user_info
+         _opts
        ) do
     data
     |> Kernel.put_in([:source, :privacy], user.default_scope)
@@ -239,6 +239,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
   defp maybe_put_notification_settings(data, _, _), do: data
 
+  defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do
+    Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move)
+  end
+
+  defp maybe_put_allow_following_move(data, _, _), do: data
+
   defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
     Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)
   end
index 5e3dbe728335d09dbd70b641bcb22c0e63349dc6..ddd7f531826d4bb18bc944770abd31b83be66b7a 100644 (file)
@@ -37,32 +37,24 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
       }
 
       case mastodon_type do
-        "mention" ->
-          response
-          |> Map.merge(%{
-            status: StatusView.render("show.json", %{activity: activity, for: user})
-          })
-
-        "favourite" ->
-          response
-          |> Map.merge(%{
-            status: StatusView.render("show.json", %{activity: parent_activity, for: user})
-          })
-
-        "reblog" ->
-          response
-          |> Map.merge(%{
-            status: StatusView.render("show.json", %{activity: parent_activity, for: user})
-          })
-
-        "follow" ->
-          response
-
-        _ ->
-          nil
+        "mention" -> put_status(response, activity, user)
+        "favourite" -> put_status(response, parent_activity, user)
+        "reblog" -> put_status(response, parent_activity, user)
+        "move" -> put_target(response, activity, user)
+        "follow" -> response
+        _ -> nil
       end
     else
       _ -> nil
     end
   end
+
+  defp put_status(response, activity, user) do
+    Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
+  end
+
+  defp put_target(response, activity, user) do
+    target = User.get_cached_by_ap_id(activity.data["target"])
+    Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
+  end
 end
index baff5415113a7d8230dce1d40d23a5dc16511b4c..a0257dfa6a0ee1705edf57ecb549e5270476aeba 100644 (file)
@@ -9,8 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
   alias Pleroma.Activity
   alias Pleroma.ActivityExpiration
-  alias Pleroma.Conversation
-  alias Pleroma.Conversation.Participation
   alias Pleroma.HTML
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -245,12 +243,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     direct_conversation_id =
       with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
            {_, 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
+           {_, %User{} = for_user} <- {:for_user, opts[:for]} do
+        Activity.direct_conversation_id(activity, for_user)
       else
         {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
           participation_id
index 12a7c2365446e8ba67754b3de17af8a25bc14cd7..01ec7941e5f6517700166f9ca28badedf2870984 100644 (file)
@@ -11,7 +11,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPubController
-  alias Pleroma.Web.ActivityPub.ObjectView
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.Endpoint
   alias Pleroma.Web.Metadata.PlayerView
@@ -38,11 +37,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do
     with id <- o_status_url(conn, :object, uuid),
          {_, %Activity{} = activity} <-
            {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
-         {_, true} <- {:public?, Visibility.is_public?(activity)},
-         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+         {_, true} <- {:public?, Visibility.is_public?(activity)} do
       case format do
-        "html" -> redirect(conn, to: "/notice/#{activity.id}")
-        _ -> represent_activity(conn, nil, activity, user)
+        _ -> redirect(conn, to: "/notice/#{activity.id}")
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -61,11 +58,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
     with id <- o_status_url(conn, :activity, uuid),
          {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
-         {_, true} <- {:public?, Visibility.is_public?(activity)},
-         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+         {_, true} <- {:public?, Visibility.is_public?(activity)} do
       case format do
-        "html" -> redirect(conn, to: "/notice/#{activity.id}")
-        _ -> represent_activity(conn, format, activity, user)
+        _ -> redirect(conn, to: "/notice/#{activity.id}")
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -81,7 +76,15 @@ defmodule Pleroma.Web.OStatus.OStatusController do
          {_, true} <- {:public?, Visibility.is_public?(activity)},
          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       cond do
-        format == "html" && activity.data["type"] == "Create" ->
+        format in ["json", "activity+json"] ->
+          if activity.local do
+            %{data: %{"id" => redirect_url}} = Object.normalize(activity)
+            redirect(conn, external: redirect_url)
+          else
+            {:error, :not_found}
+          end
+
+        activity.data["type"] == "Create" ->
           %Object{} = object = Object.normalize(activity)
 
           RedirectController.redirector_with_meta(
@@ -94,11 +97,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
             }
           )
 
-        format == "html" ->
-          RedirectController.redirector(conn, nil)
-
         true ->
-          represent_activity(conn, format, activity, user)
+          RedirectController.redirector(conn, nil)
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -135,24 +135,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
     end
   end
 
-  defp represent_activity(
-         conn,
-         "activity+json",
-         %Activity{data: %{"type" => "Create"}} = activity,
-         _user
-       ) do
-    object = Object.normalize(activity)
-
-    conn
-    |> put_resp_header("content-type", "application/activity+json")
-    |> put_view(ObjectView)
-    |> render("object.json", %{object: object})
-  end
-
-  defp represent_activity(_conn, _, _, _) do
-    {:error, :not_found}
-  end
-
   def errors(conn, {:error, :not_found}) do
     render_error(conn, :not_found, "Not found")
   end
index dd445e8bfe2d0877ebbe850b304b5ef6952f2979..a6a924d02f067191ddda7e157ffbb40209fd2f12 100644 (file)
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.Push.Impl do
   require Logger
   import Ecto.Query
 
-  @types ["Create", "Follow", "Announce", "Like"]
+  @types ["Create", "Follow", "Announce", "Like", "Move"]
 
   @doc "Performs sending notifications for user subscriptions"
   @spec perform(Notification.t()) :: list(any) | :error
@@ -33,6 +33,8 @@ defmodule Pleroma.Web.Push.Impl do
     gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
     avatar_url = User.avatar_url(actor)
     object = Object.normalize(activity)
+    user = User.get_cached_by_id(user_id)
+    direct_conversation_id = Activity.direct_conversation_id(activity, user)
 
     for subscription <- fetch_subsriptions(user_id),
         get_in(subscription.data, ["alerts", type]) do
@@ -45,7 +47,8 @@ defmodule Pleroma.Web.Push.Impl do
         icon: avatar_url,
         preferred_locale: "en",
         pleroma: %{
-          activity_id: activity_id
+          activity_id: activity_id,
+          direct_conversation_id: direct_conversation_id
         }
       }
       |> Jason.encode!()
index 129da422c2b3a4a13b9aee773859f2491910fe39..e6c4f6f1495d1c14c6c1eac0218a325e02ab89d5 100644 (file)
@@ -178,6 +178,11 @@ defmodule Pleroma.Web.Router do
     get("/users/:nickname", AdminAPIController, :user_show)
     get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
 
+    get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
+
+    patch("/users/confirm_email", AdminAPIController, :confirm_email)
+    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
+
     get("/reports", AdminAPIController, :list_reports)
     get("/grouped_reports", AdminAPIController, :list_grouped_reports)
     get("/reports/:id", AdminAPIController, :report_show)
index 7ffc8eabec217ae19db9ec72c5465353526b4eeb..323a4da1ea2d73ae1e2e56af42cd92dcbacb0a76 100644 (file)
@@ -71,4 +71,11 @@ defmodule Pleroma.Workers.BackgroundWorker do
     activity = Activity.get_by_id(activity_id)
     Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
   end
+
+  def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do
+    origin = User.get_cached_by_id(origin_id)
+    target = User.get_cached_by_id(target_id)
+
+    Pleroma.FollowingRelationship.move_following(origin, target)
+  end
 end
diff --git a/mix.exs b/mix.exs
index 81ce4f25cef74d619776b7a7b6b18a569ff014f8..eb2e54e4ab0575afff184726cfbf55e8b017766a 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -102,7 +102,7 @@ defmodule Pleroma.Mixfile do
       {:phoenix_ecto, "~> 4.0"},
       {:ecto_sql, "~> 3.2"},
       {:postgrex, ">= 0.13.5"},
-      {:oban, "~> 0.8.1"},
+      {:oban, "~> 0.12.0"},
       {:quantum, "~> 2.3"},
       {:gettext, "~> 0.15"},
       {:comeonin, "~> 4.1.1"},
@@ -194,27 +194,21 @@ defmodule Pleroma.Mixfile do
     identifier_filter = ~r/[^0-9a-z\-]+/i
 
     # Pre-release version, denoted from patch version with a hyphen
-    {git_tag, git_pre_release} =
+    git_pre_release =
       with {tag, 0} <-
              System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
-           tag = String.trim(tag),
-           {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
-           describe = String.trim(describe),
-           ahead <- String.replace(describe, tag, ""),
-           ahead <- String.trim_leading(ahead, "-") do
-        {String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
+           {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
+        describe
+        |> String.trim()
+        |> String.replace(String.trim(tag), "")
+        |> String.trim_leading("-")
+        |> String.trim()
       else
         _ ->
           {commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
-          {nil, "0-g" <> String.trim(commit_hash)}
+          "0-g" <> String.trim(commit_hash)
       end
 
-    if git_tag && version != git_tag do
-      Mix.shell().error(
-        "Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
-      )
-    end
-
     # Branch name as pre-release version component, denoted with a dot
     branch_name =
       with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
index 3a664287c2b544045abc70d44a04fa2ad4427c76..49bdad128b9d75f46e63b56df6020f1208ccb196 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -23,8 +23,8 @@
   "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
   "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
   "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
-  "ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
-  "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+  "ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
+  "ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
   "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
   "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
   "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@@ -38,7 +38,7 @@
   "fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"},
   "fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
   "flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
-  "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
+  "floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
   "gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"},
   "gen_stage": {:hex, :gen_stage, "0.14.3", "d0c66f1c87faa301c1a85a809a3ee9097a4264b2edf7644bf5c123237ef732bf", [:mix], [], "hexpm"},
   "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
@@ -67,7 +67,7 @@
   "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
-  "oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+  "oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"},
   "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
@@ -97,7 +97,7 @@
   "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
   "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
   "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
-  "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
+  "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
   "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
   "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
   "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
diff --git a/priv/repo/migrations/20191025081729_add_move_support_to_users.exs b/priv/repo/migrations/20191025081729_add_move_support_to_users.exs
new file mode 100644 (file)
index 0000000..580b9eb
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.AddMoveSupportToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add(:also_known_as, {:array, :string}, default: [], null: false)
+      add(:allow_following_move, :boolean, default: true, null: false)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20191104133100_set_visible_service_actors.exs b/priv/repo/migrations/20191104133100_set_visible_service_actors.exs
new file mode 100644 (file)
index 0000000..6290709
--- /dev/null
@@ -0,0 +1,22 @@
+defmodule Pleroma.Repo.Migrations.SetVisibleServiceActors do
+  use Ecto.Migration
+  import Ecto.Query
+  alias Pleroma.Repo
+
+  def up do
+    user_nicknames = ["relay", "internal.fetch"]
+
+    from(
+      u in "users",
+      where: u.nickname in ^user_nicknames,
+      update: [
+        set: [invisible: true]
+      ]
+    )
+    |> Repo.update_all([])
+  end
+
+  def down do
+    :ok
+  end
+end
diff --git a/priv/repo/migrations/20191123103423_remove_info_from_users.exs b/priv/repo/migrations/20191123103423_remove_info_from_users.exs
new file mode 100644 (file)
index 0000000..b251255
--- /dev/null
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveInfoFromUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      remove(:info, :map, default: %{})
+    end
+  end
+end
diff --git a/priv/repo/migrations/20191128153944_fix_missing_following_count.exs b/priv/repo/migrations/20191128153944_fix_missing_following_count.exs
new file mode 100644 (file)
index 0000000..3236de7
--- /dev/null
@@ -0,0 +1,53 @@
+defmodule Pleroma.Repo.Migrations.FixMissingFollowingCount do
+  use Ecto.Migration
+
+  def up do
+    """
+    UPDATE
+      users
+    SET
+      following_count = sub.count
+    FROM
+      (
+        SELECT
+          users.id AS sub_id
+          ,COUNT (following_relationships.id)
+        FROM
+          following_relationships
+          ,users
+        WHERE
+          users.id = following_relationships.follower_id
+        AND following_relationships.state = 'accept'
+        GROUP BY
+          users.id
+      ) AS sub
+    WHERE
+      users.id = sub.sub_id
+    AND users.local = TRUE
+    ;
+    """
+    |> execute()
+
+    """
+    UPDATE
+      users
+    SET
+      following_count = 0
+    WHERE
+      following_count IS NULL
+    """
+    |> execute()
+
+    execute("ALTER TABLE users
+      ALTER COLUMN following_count SET DEFAULT 0,
+      ALTER COLUMN following_count SET NOT NULL
+    ")
+  end
+
+  def down do
+    execute("ALTER TABLE users
+      ALTER COLUMN following_count DROP DEFAULT,
+      ALTER COLUMN following_count DROP NOT NULL
+    ")
+  end
+end
index 8bae42f6da9e4e80a877a58b864fc2131e663d7e..e7ebf72be15630be73d62c53d244374ee7101946 100644 (file)
                 "@id": "litepub:oauthRegistrationEndpoint",
                 "@type": "@id"
             },
-            "EmojiReaction": "litepub:EmojiReaction"
+            "EmojiReaction": "litepub:EmojiReaction",
+            "alsoKnownAs": {
+                "@id": "as:alsoKnownAs",
+                "@type": "@id"
+            }
         }
     ]
 }
index a4ce2529531778b8be50d53334a31a90816e728e..e1b87102d15207295dd9ed6a7199725b4d5b43e0 100644 (file)
@@ -8,5 +8,5 @@
 # fi
 
 # Set the release to work across nodes
-export RELEASE_DISTRIBUTION=name
-export RELEASE_NODE=<%= @release.name %>@127.0.0.1
+export RELEASE_DISTRIBUTION="${RELEASE_DISTRIBUTION:-name}"
+export RELEASE_NODE="${RELEASE_NODE:-<%= @release.name %>@127.0.0.1}"
diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
new file mode 100644 (file)
index 0000000..4580056
--- /dev/null
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright Â© 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Integration.FederationTest do
+  use Pleroma.DataCase
+  @moduletag :federated
+  import Pleroma.Cluster
+
+  setup_all do
+    Pleroma.Cluster.spawn_default_cluster()
+    :ok
+  end
+
+  @federated1 :"federated1@127.0.0.1"
+  describe "federated cluster primitives" do
+    test "within/2 captures local bindings and executes block on remote node" do
+      captured_binding = :captured
+
+      result =
+        within @federated1 do
+          user = Pleroma.Factory.insert(:user)
+          {captured_binding, node(), user}
+        end
+
+      assert {:captured, @federated1, user} = result
+      refute Pleroma.User.get_by_id(user.id)
+      assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id
+    end
+
+    test "runs webserver on customized port" do
+      {nickname, url, url_404} =
+        within @federated1 do
+          import Pleroma.Web.Router.Helpers
+          user = Pleroma.Factory.insert(:user)
+          user_url = account_url(Pleroma.Web.Endpoint, :show, user)
+          url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists")
+
+          {user.nickname, user_url, url_404}
+        end
+
+      assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}")
+      assert %{"acct" => ^nickname} = Jason.decode!(body)
+      assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}")
+    end
+  end
+end
index 8159dc20ad0e3c0ba52e7e16ba2fcbc0be4ee1f8..9fdd6557ca26095df858b17b4dd7e6adf58d8aaf 100644 (file)
@@ -9,7 +9,11 @@
     "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
     "conversation": "ostatus:conversation",
     "toot": "http://joinmastodon.org/ns#",
-    "Emoji": "toot:Emoji"
+    "Emoji": "toot:Emoji",
+    "alsoKnownAs": {
+      "@id": "as:alsoKnownAs",
+      "@type": "@id"
+    }
   }],
   "id": "http://mastodon.example.org/users/admin",
   "type": "Person",
@@ -50,5 +54,6 @@
     "type": "Image",
     "mediaType": "image/png",
     "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
-  }
+  },
+  "alsoKnownAs": ["http://example.org/users/foo"]
 }
diff --git a/test/fixtures/users_mock/friendica_followers.json b/test/fixtures/users_mock/friendica_followers.json
new file mode 100644 (file)
index 0000000..7b86b5f
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "vcard": "http://www.w3.org/2006/vcard/ns#",
+      "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+      "diaspora": "https://diasporafoundation.org/ns/",
+      "litepub": "http://litepub.social/ns#",
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "sensitive": "as:sensitive",
+      "Hashtag": "as:Hashtag",
+      "directMessage": "litepub:directMessage"
+    }
+  ],
+  "id": "http://localhost:8080/followers/fuser3",
+  "type": "OrderedCollection",
+  "totalItems": 296
+}
diff --git a/test/fixtures/users_mock/friendica_following.json b/test/fixtures/users_mock/friendica_following.json
new file mode 100644 (file)
index 0000000..7c526be
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "vcard": "http://www.w3.org/2006/vcard/ns#",
+      "dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
+      "diaspora": "https://diasporafoundation.org/ns/",
+      "litepub": "http://litepub.social/ns#",
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "sensitive": "as:sensitive",
+      "Hashtag": "as:Hashtag",
+      "directMessage": "litepub:directMessage"
+    }
+  ],
+  "id": "http://localhost:8080/following/fuser3",
+  "type": "OrderedCollection",
+  "totalItems": 32
+}
diff --git a/test/following_relationship_test.exs b/test/following_relationship_test.exs
new file mode 100644 (file)
index 0000000..93c0798
--- /dev/null
@@ -0,0 +1,47 @@
+# Pleroma: A lightweight social networking server
+# Copyright Â© 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.FollowingRelationshipTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.FollowingRelationship
+  alias Pleroma.Web.ActivityPub.InternalFetchActor
+  alias Pleroma.Web.ActivityPub.Relay
+
+  import Pleroma.Factory
+
+  describe "following/1" do
+    test "returns following addresses without internal.fetch" do
+      user = insert(:user)
+      fetch_actor = InternalFetchActor.get_actor()
+      FollowingRelationship.follow(fetch_actor, user, "accept")
+      assert FollowingRelationship.following(fetch_actor) == [user.follower_address]
+    end
+
+    test "returns following addresses without relay" do
+      user = insert(:user)
+      relay_actor = Relay.get_actor()
+      FollowingRelationship.follow(relay_actor, user, "accept")
+      assert FollowingRelationship.following(relay_actor) == [user.follower_address]
+    end
+
+    test "returns following addresses without remote user" do
+      user = insert(:user)
+      actor = insert(:user, local: false)
+      FollowingRelationship.follow(actor, user, "accept")
+      assert FollowingRelationship.following(actor) == [user.follower_address]
+    end
+
+    test "returns following addresses with local user" do
+      user = insert(:user)
+      actor = insert(:user, local: true)
+      FollowingRelationship.follow(actor, user, "accept")
+
+      assert FollowingRelationship.following(actor) == [
+               actor.follower_address,
+               user.follower_address
+             ]
+    end
+  end
+end
index f0869534cdced6047d9ba72be9f1b27c3a00961b..c918dbe203156a9bac79d251b94d9ab5b95c9ce7 100644 (file)
@@ -228,5 +228,16 @@ defmodule Pleroma.HTMLTest do
 
       assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
     end
+
+    test "does not crash when there is an HTML entity in a link" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{"status" => "\"http://cofe.com/?boomer=ok&foo=bar\""})
+
+      object = Object.normalize(activity)
+
+      assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"])
+    end
   end
 end
index 170ca916f6448ae75450295cda14df38e502c346..80ef25d7b1051f6651e6750054adbe4e3a6bc284 100644 (file)
@@ -16,11 +16,21 @@ defmodule Pleroma.HTTP.RequestBuilderTest do
 
     test "send pleroma user agent" do
       Pleroma.Config.put([:http, :send_user_agent], true)
+      Pleroma.Config.put([:http, :user_agent], :default)
 
       assert RequestBuilder.headers(%{}, []) == %{
                headers: [{"User-Agent", Pleroma.Application.user_agent()}]
              }
     end
+
+    test "send custom user agent" do
+      Pleroma.Config.put([:http, :send_user_agent], true)
+      Pleroma.Config.put([:http, :user_agent], "totally-not-pleroma")
+
+      assert RequestBuilder.headers(%{}, []) == %{
+               headers: [{"User-Agent", "totally-not-pleroma"}]
+             }
+    end
   end
 
   describe "add_optional_params/3" do
index f8d42922322353a2d509dc8fc2c4c33313db186a..dcbffeafe39b16c1afdf7505fc4744cf2119ce25 100644 (file)
@@ -630,6 +630,35 @@ defmodule Pleroma.NotificationTest do
 
       assert Enum.empty?(Notification.for_user(local_user))
     end
+
+    test "move activity generates a notification" do
+      %{ap_id: old_ap_id} = old_user = insert(:user)
+      %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+      follower = insert(:user)
+      other_follower = insert(:user, %{allow_following_move: false})
+
+      User.follow(follower, old_user)
+      User.follow(other_follower, old_user)
+
+      Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+      ObanHelpers.perform_all()
+
+      assert [
+               %{
+                 activity: %{
+                   data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+                 }
+               }
+             ] = Notification.for_user(follower)
+
+      assert [
+               %{
+                 activity: %{
+                   data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
+                 }
+               }
+             ] = Notification.for_user(other_follower)
+    end
   end
 
   describe "for_user" do
diff --git a/test/support/cluster.ex b/test/support/cluster.ex
new file mode 100644 (file)
index 0000000..deb37f3
--- /dev/null
@@ -0,0 +1,218 @@
+defmodule Pleroma.Cluster do
+  @moduledoc """
+  Facilities for managing a cluster of slave VM's for federated testing.
+
+  ## Spawning the federated cluster
+
+  `spawn_cluster/1` spawns a map of slave nodes that are started
+  within the running VM. During startup, the slave node is sent all configuration
+  from the parent node, as well as all code. After receiving configuration and
+  code, the slave then starts all applications currently running on the parent.
+  The configuration passed to `spawn_cluster/1` overrides any parent application
+  configuration for the provided OTP app and key. This is useful for customizing
+  the Ecto database, Phoenix webserver ports, etc.
+
+  For example, to start a single federated VM named ":federated1", with the
+  Pleroma Endpoint running on port 4123, and with a database named
+  "pleroma_test1", you would run:
+
+    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+    Pleroma.Cluster.spawn_cluster(%{
+      :"federated1@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
+        {:pleroma, Pleroma.Web.Endpoint,
+        Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+      ]
+    })
+
+  *Note*: application configuration for a given key is not merged,
+  so any customization requires first fetching the existing values
+  and merging yourself by providing the merged configuration,
+  such as above with the endpoint config and repo config.
+
+  ## Executing code within a remote node
+
+  Use the `within/2` macro to execute code within the context of a remote
+  federated node. The code block captures all local variable bindings from
+  the parent's context and returns the result of the expression after executing
+  it on the remote node. For example:
+
+      import Pleroma.Cluster
+
+      parent_value = 123
+
+      result =
+        within :"federated1@127.0.0.1" do
+          {node(), parent_value}
+        end
+
+      assert result == {:"federated1@127.0.0.1, 123}
+
+  *Note*: while local bindings are captured and available within the block,
+  other parent contexts like required, aliased, or imported modules are not
+  in scope. Those will need to be reimported/aliases/required within the block
+  as `within/2` is a remote procedure call.
+  """
+
+  @extra_apps Pleroma.Mixfile.application()[:extra_applications]
+
+  @doc """
+  Spawns the default Pleroma federated cluster.
+
+  Values before may be customized as needed for the test suite.
+  """
+  def spawn_default_cluster do
+    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+    spawn_cluster(%{
+      :"federated1@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
+        {:pleroma, Pleroma.Web.Endpoint,
+         Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+      ],
+      :"federated2@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
+        {:pleroma, Pleroma.Web.Endpoint,
+         Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
+      ]
+    })
+  end
+
+  @doc """
+  Spawns a configured map of federated nodes.
+
+  See `Pleroma.Cluster` module documentation for details.
+  """
+  def spawn_cluster(node_configs) do
+    # Turn node into a distributed node with the given long name
+    :net_kernel.start([:"primary@127.0.0.1"])
+
+    # Allow spawned nodes to fetch all code from this node
+    {:ok, _} = :erl_boot_server.start([])
+    allow_boot("127.0.0.1")
+
+    silence_logger_warnings(fn ->
+      node_configs
+      |> Enum.map(&Task.async(fn -> start_slave(&1) end))
+      |> Enum.map(&Task.await(&1, 60_000))
+    end)
+  end
+
+  @doc """
+  Executes block of code again remote node.
+
+  See `Pleroma.Cluster` module documentation for details.
+  """
+  defmacro within(node, do: block) do
+    quote do
+      rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
+        unquote(Macro.escape(block)),
+        binding()
+      ])
+    end
+  end
+
+  @doc false
+  def eval_quoted(block, binding) do
+    {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
+    result
+  end
+
+  defp start_slave({node_host, override_configs}) do
+    log(node_host, "booting federated VM")
+    {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
+    add_code_paths(node)
+    load_apps_and_transfer_configuration(node, override_configs)
+    ensure_apps_started(node)
+    {:ok, node}
+  end
+
+  def rpc(node, module, function, args) do
+    :rpc.block_call(node, module, function, args)
+  end
+
+  defp vm_args do
+    ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
+  end
+
+  defp allow_boot(host) do
+    {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
+    :ok = :erl_boot_server.add_slave(ipv4)
+  end
+
+  defp add_code_paths(node) do
+    rpc(node, :code, :add_paths, [:code.get_path()])
+  end
+
+  defp load_apps_and_transfer_configuration(node, override_configs) do
+    Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
+      app_name
+      |> Application.get_all_env()
+      |> Enum.each(fn {key, primary_config} ->
+        rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
+      end)
+    end)
+
+    Enum.each(override_configs, fn {app_name, key, val} ->
+      rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
+    end)
+  end
+
+  defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
+
+  defp ensure_apps_started(node) do
+    loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
+    app_names = @extra_apps ++ (loaded_names -- @extra_apps)
+
+    rpc(node, Application, :ensure_all_started, [:mix])
+    rpc(node, Mix, :env, [Mix.env()])
+    rpc(node, __MODULE__, :prepare_database, [])
+
+    log(node, "starting application")
+
+    Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
+      if Enum.member?(loaded, app) do
+        loaded
+      else
+        {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
+        MapSet.union(loaded, MapSet.new(started))
+      end
+    end)
+  end
+
+  @doc false
+  def prepare_database do
+    log(node(), "preparing database")
+    repo_config = Application.get_env(:pleroma, Pleroma.Repo)
+    repo_config[:adapter].storage_down(repo_config)
+    repo_config[:adapter].storage_up(repo_config)
+
+    {:ok, _, _} =
+      Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+        Ecto.Migrator.run(repo, :up, log: false, all: true)
+      end)
+
+    Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
+    {:ok, _} = Application.ensure_all_started(:ex_machina)
+  end
+
+  defp silence_logger_warnings(func) do
+    prev_level = Logger.level()
+    Logger.configure(level: :error)
+    res = func.()
+    Logger.configure(level: prev_level)
+
+    res
+  end
+
+  defp node_name(node_host) do
+    node_host
+    |> to_string()
+    |> String.split("@")
+    |> Enum.at(0)
+    |> String.to_atom()
+  end
+end
index e3f797f64c16b4abb0dcc8f05ada8cd8a811479f..bb8a64e7243816e5b9a036e25ec458311eda8c72 100644 (file)
@@ -31,7 +31,6 @@ defmodule Pleroma.Factory do
       nickname: sequence(:nickname, &"nick#{&1}"),
       password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
       bio: sequence(:bio, &"Tester Number #{&1}"),
-      info: %{},
       last_digest_emailed_at: NaiveDateTime.utc_now()
     }
 
index 965335e966bed52e72b7a0d90d69e59fc6ebcb37..e3a621f49ac0fcabc08fc51cda0109d116fc1114 100644 (file)
@@ -1035,6 +1035,22 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("http://localhost:8080/followers/fuser3", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/users_mock/friendica_followers.json")
+     }}
+  end
+
+  def get("http://localhost:8080/following/fuser3", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/users_mock/friendica_following.json")
+     }}
+  end
+
   def get("http://localhost:4001/users/fuser2/followers", _, _, _) do
     {:ok,
      %Tesla.Env{
index c8dbee0108511d51f5fb9e401d76dc850c6e67f4..241ad1f94d2cc39a10a02359e7423645eee085ca 100644 (file)
@@ -3,7 +3,8 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
-ExUnit.start(exclude: os_exclude)
+ExUnit.start(exclude: [:federated | os_exclude])
+
 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
 Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
 {:ok, _} = Application.ensure_all_started(:ex_machina)
index 721af1e5bb9bbecc1d677f3309dceb8126d1d1dd..98841dbbda2c231f586a3b17896ca08815f2b737 100644 (file)
@@ -15,6 +15,14 @@ defmodule Pleroma.UserSearchTest do
   end
 
   describe "User.search" do
+    test "excluded invisible users from results" do
+      user = insert(:user, %{nickname: "john t1000"})
+      insert(:user, %{invisible: true, nickname: "john t800"})
+
+      [found_user] = User.search("john")
+      assert found_user.id == user.id
+    end
+
     test "accepts limit parameter" do
       Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
       assert length(User.search("john", limit: 3)) == 3
index 8fdb6b25fc2f2bba9916d8bfdc32c4924fb00bf4..c345e43e9168a475bb291013208a16b48bf3f1b1 100644 (file)
@@ -25,6 +25,25 @@ defmodule Pleroma.UserTest do
 
   clear_config([:instance, :account_activation_required])
 
+  describe "service actors" do
+    test "returns invisible actor" do
+      uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
+      followers_uri = "#{uri}/followers"
+      user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+
+      assert %User{
+               nickname: "internal.fetch-test",
+               invisible: true,
+               local: true,
+               ap_id: ^uri,
+               follower_address: ^followers_uri
+             } = user
+
+      user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
+      assert user.id == user2.id
+    end
+  end
+
   describe "when tags are nil" do
     test "tagging a user" do
       user = insert(:user, %{tags: nil})
@@ -148,9 +167,10 @@ defmodule Pleroma.UserTest do
     {:ok, user} = User.follow(user, followed)
 
     user = User.get_cached_by_id(user.id)
-
     followed = User.get_cached_by_ap_id(followed.ap_id)
+
     assert followed.follower_count == 1
+    assert user.following_count == 1
 
     assert User.ap_followers(followed) in User.following(user)
   end
@@ -347,18 +367,6 @@ defmodule Pleroma.UserTest do
 
       assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
     end
-
-    test "it ensures info is not nil" do
-      changeset = User.register_changeset(%User{}, @full_user_data)
-
-      assert changeset.valid?
-
-      {:ok, user} =
-        changeset
-        |> Repo.insert()
-
-      refute is_nil(user.info)
-    end
   end
 
   describe "user registration, with :account_activation_required" do
@@ -412,8 +420,7 @@ defmodule Pleroma.UserTest do
           :user,
           local: false,
           nickname: "admin@mastodon.example.org",
-          ap_id: ap_id,
-          info: %{}
+          ap_id: ap_id
         )
 
       {:ok, fetched_user} = User.get_or_fetch(ap_id)
@@ -474,8 +481,7 @@ defmodule Pleroma.UserTest do
           local: false,
           nickname: "admin@mastodon.example.org",
           ap_id: "http://mastodon.example.org/users/admin",
-          last_refreshed_at: a_week_ago,
-          info: %{}
+          last_refreshed_at: a_week_ago
         )
 
       assert orig_user.last_refreshed_at == a_week_ago
@@ -516,7 +522,6 @@ defmodule Pleroma.UserTest do
       name: "Someone",
       nickname: "a@b.de",
       ap_id: "http...",
-      info: %{some: "info"},
       avatar: %{some: "avatar"}
     }
 
@@ -941,9 +946,9 @@ defmodule Pleroma.UserTest do
       {:ok, user} = User.follow(user, user2)
       {:ok, _user} = User.deactivate(user)
 
-      info = User.get_cached_user_info(user2)
+      user2 = User.get_cached_by_id(user2.id)
 
-      assert info.follower_count == 0
+      assert user2.follower_count == 0
       assert [] = User.get_followers(user2)
     end
 
@@ -952,13 +957,15 @@ defmodule Pleroma.UserTest do
       user2 = insert(:user)
 
       {:ok, user2} = User.follow(user2, user)
+      assert user2.following_count == 1
       assert User.following_count(user2) == 1
 
       {:ok, _user} = User.deactivate(user)
 
-      info = User.get_cached_user_info(user2)
+      user2 = User.get_cached_by_id(user2.id)
 
-      assert info.following_count == 0
+      assert refresh_record(user2).following_count == 0
+      assert user2.following_count == 0
       assert User.following_count(user2) == 0
       assert [] = User.get_friends(user2)
     end
@@ -1121,8 +1128,7 @@ defmodule Pleroma.UserTest do
         ap_id: user.ap_id,
         name: user.name,
         nickname: user.nickname,
-        bio: String.duplicate("h", current_max_length + 1),
-        info: %{}
+        bio: String.duplicate("h", current_max_length + 1)
       }
 
       assert {:ok, %User{}} = User.insert_or_update_user(data)
@@ -1135,8 +1141,7 @@ defmodule Pleroma.UserTest do
       data = %{
         ap_id: user.ap_id,
         name: String.duplicate("h", current_max_length + 1),
-        nickname: user.nickname,
-        info: %{}
+        nickname: user.nickname
       }
 
       assert {:ok, %User{}} = User.insert_or_update_user(data)
@@ -1160,13 +1165,12 @@ defmodule Pleroma.UserTest do
   describe "caching" do
     test "invalidate_cache works" do
       user = insert(:user)
-      _user_info = User.get_cached_user_info(user)
 
+      User.set_cache(user)
       User.invalidate_cache(user)
 
       {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
       {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
-      {:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
     end
 
     test "User.delete() plugs any possible zombie objects" do
@@ -1322,7 +1326,7 @@ defmodule Pleroma.UserTest do
 
     {:ok, user} = User.block(user, follower)
 
-    assert User.user_info(user).follower_count == 2
+    assert user.follower_count == 2
   end
 
   describe "list_inactive_users_query/1" do
@@ -1499,51 +1503,6 @@ defmodule Pleroma.UserTest do
     end
   end
 
-  describe "set_info_cache/2" do
-    setup do
-      user = insert(:user)
-      {:ok, user: user}
-    end
-
-    test "update from args", %{user: user} do
-      User.set_info_cache(user, %{following_count: 15, follower_count: 18})
-
-      %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-      assert followers == 18
-      assert following == 15
-    end
-
-    test "without args", %{user: user} do
-      User.set_info_cache(user, %{})
-
-      %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-      assert followers == 0
-      assert following == 0
-    end
-  end
-
-  describe "user_info/2" do
-    setup do
-      user = insert(:user)
-      {:ok, user: user}
-    end
-
-    test "update from args", %{user: user} do
-      %{follower_count: followers, following_count: following} =
-        User.user_info(user, %{following_count: 15, follower_count: 18})
-
-      assert followers == 18
-      assert following == 15
-    end
-
-    test "without args", %{user: user} do
-      %{follower_count: followers, following_count: following} = User.user_info(user)
-
-      assert followers == 0
-      assert following == 0
-    end
-  end
-
   describe "is_internal_user?/1" do
     test "non-internal user returns false" do
       user = insert(:user)
@@ -1600,14 +1559,14 @@ defmodule Pleroma.UserTest do
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      assert other_user.following_count == 0
+      assert other_user.follower_count == 0
 
       {:ok, user} = Pleroma.User.follow(user, other_user)
       other_user = Pleroma.User.get_by_id(other_user.id)
 
-      assert User.user_info(user).following_count == 1
-      assert User.user_info(other_user).follower_count == 1
+      assert user.following_count == 1
+      assert other_user.follower_count == 1
     end
 
     test "syncronizes the counters with the remote instance for the followed when enabled" do
@@ -1623,14 +1582,14 @@ defmodule Pleroma.UserTest do
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      assert other_user.following_count == 0
+      assert other_user.follower_count == 0
 
       Pleroma.Config.put([:instance, :external_user_synchronization], true)
       {:ok, _user} = User.follow(user, other_user)
       other_user = User.get_by_id(other_user.id)
 
-      assert User.user_info(other_user).follower_count == 437
+      assert other_user.follower_count == 437
     end
 
     test "syncronizes the counters with the remote instance for the follower when enabled" do
@@ -1646,13 +1605,13 @@ defmodule Pleroma.UserTest do
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      assert other_user.following_count == 0
+      assert other_user.follower_count == 0
 
       Pleroma.Config.put([:instance, :external_user_synchronization], true)
       {:ok, other_user} = User.follow(other_user, user)
 
-      assert User.user_info(other_user).following_count == 152
+      assert other_user.following_count == 152
     end
   end
 
index a5414c5210bc66b6f38c63ce70f14a07416bc6a8..1aa73d75cf675efee5dfd1df83028840b92324b5 100644 (file)
@@ -110,6 +110,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
 
       assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
     end
+
+    test "it returns 404 for remote users", %{
+      conn: conn
+    } do
+      user = insert(:user, local: false, nickname: "remoteuser@example.com")
+
+      conn =
+        conn
+        |> put_req_header("accept", "application/json")
+        |> get("/users/#{user.nickname}.json")
+
+      assert json_response(conn, 404)
+    end
   end
 
   describe "/object/:uuid" do
index d437ad456c78a8d1c85e739748fbd321605d6021..2677b9e36bbd48b1a700a2dc9f5811f997dc237a 100644 (file)
@@ -4,8 +4,11 @@
 
 defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   use Pleroma.DataCase
+  use Oban.Testing, repo: Pleroma.Repo
+
   alias Pleroma.Activity
   alias Pleroma.Builders.ActivityBuilder
+  alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -1554,5 +1557,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert follow_info.hide_followers == false
       assert follow_info.hide_follows == true
     end
+
+    test "detects hidden follows/followers for friendica" do
+      user =
+        insert(:user,
+          local: false,
+          follower_address: "http://localhost:8080/followers/fuser3",
+          following_address: "http://localhost:8080/following/fuser3"
+        )
+
+      {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+      assert follow_info.hide_followers == true
+      assert follow_info.follower_count == 296
+      assert follow_info.following_count == 32
+      assert follow_info.hide_follows == true
+    end
+  end
+
+  describe "Move activity" do
+    test "create" do
+      %{ap_id: old_ap_id} = old_user = insert(:user)
+      %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+      follower = insert(:user)
+      follower_move_opted_out = insert(:user, allow_following_move: false)
+
+      User.follow(follower, old_user)
+      User.follow(follower_move_opted_out, old_user)
+
+      assert User.following?(follower, old_user)
+      assert User.following?(follower_move_opted_out, old_user)
+
+      assert {:ok, activity} = ActivityPub.move(old_user, new_user)
+
+      assert %Activity{
+               actor: ^old_ap_id,
+               data: %{
+                 "actor" => ^old_ap_id,
+                 "object" => ^old_ap_id,
+                 "target" => ^new_ap_id,
+                 "type" => "Move"
+               },
+               local: true
+             } = activity
+
+      params = %{
+        "op" => "move_following",
+        "origin_id" => old_user.id,
+        "target_id" => new_user.id
+      }
+
+      assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
+
+      Pleroma.Workers.BackgroundWorker.perform(params, nil)
+
+      refute User.following?(follower, old_user)
+      assert User.following?(follower, new_user)
+
+      assert User.following?(follower_move_opted_out, old_user)
+      refute User.following?(follower_move_opted_out, new_user)
+
+      activity = %Activity{activity | object: nil}
+
+      assert [%Notification{activity: ^activity}] =
+               Notification.for_user_since(follower, ~N[2019-04-13 11:22:33])
+
+      assert [%Notification{activity: ^activity}] =
+               Notification.for_user_since(follower_move_opted_out, ~N[2019-04-13 11:22:33])
+    end
+
+    test "old user must be in the new user's `also_known_as` list" do
+      old_user = insert(:user)
+      new_user = insert(:user)
+
+      assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
+               ActivityPub.move(old_user, new_user)
+    end
   end
 end
index 0bdd514e97384edf5a81c40bcf692185b2a1cbfd..5da358c43636e10fa0556dfbb3d4b72393c43bfe 100644 (file)
@@ -39,6 +39,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert activity == returned_activity
     end
 
+    @tag capture_log: true
     test "it fetches replied-to activities if we don't have them" do
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
@@ -533,6 +534,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert object.data["content"] == "this is a private toot"
     end
 
+    @tag capture_log: true
     test "it rejects incoming announces with an inlined activity from another origin" do
       data =
         File.read!("test/fixtures/bogus-mastodon-announce.json")
@@ -681,6 +683,37 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert user.bio == "<p>Some bio</p>"
     end
 
+    test "it works with alsoKnownAs" do
+      {:ok, %Activity{data: %{"actor" => actor}}} =
+        "test/fixtures/mastodon-post-activity.json"
+        |> File.read!()
+        |> Poison.decode!()
+        |> Transmogrifier.handle_incoming()
+
+      assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"]
+
+      {:ok, _activity} =
+        "test/fixtures/mastodon-update.json"
+        |> File.read!()
+        |> Poison.decode!()
+        |> Map.put("actor", actor)
+        |> Map.update!("object", fn object ->
+          object
+          |> Map.put("actor", actor)
+          |> Map.put("id", actor)
+          |> Map.put("alsoKnownAs", [
+            "http://mastodon.example.org/users/foo",
+            "http://example.org/users/bar"
+          ])
+        end)
+        |> Transmogrifier.handle_incoming()
+
+      assert User.get_cached_by_ap_id(actor).also_known_as == [
+               "http://mastodon.example.org/users/foo",
+               "http://example.org/users/bar"
+             ]
+    end
+
     test "it works with custom profile fields" do
       {:ok, activity} =
         "test/fixtures/mastodon-post-activity.json"
@@ -814,6 +847,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert Activity.get_by_id(activity.id)
     end
 
+    @tag capture_log: true
     test "it works for incoming user deletes" do
       %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
 
@@ -1269,6 +1303,30 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
       assert [user.follower_address] == activity.data["to"]
     end
+
+    test "it accepts Move activities" do
+      old_user = insert(:user)
+      new_user = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Move",
+        "actor" => old_user.ap_id,
+        "object" => old_user.ap_id,
+        "target" => new_user.ap_id
+      }
+
+      assert :error = Transmogrifier.handle_incoming(message)
+
+      {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
+
+      assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
+      assert activity.actor == old_user.ap_id
+      assert activity.data["actor"] == old_user.ap_id
+      assert activity.data["object"] == old_user.ap_id
+      assert activity.data["target"] == new_user.ap_id
+      assert activity.data["type"] == "Move"
+    end
   end
 
   describe "prepare outgoing" do
@@ -1749,6 +1807,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert modified_object["inReplyToAtomUri"] == ""
     end
 
+    @tag capture_log: true
     test "returns modified object when allowed incoming reply", %{data: data} do
       object_with_reply =
         Map.put(
@@ -1868,6 +1927,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
              end) =~ "Unsupported URI scheme"
     end
 
+    @tag capture_log: true
     test "returns {:ok, %Object{}} for success case" do
       assert {:ok, %Object{}} =
                Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
index fd179e8c208f35cd8e74cd8743c5b9b97a5c2ca8..d0131fd903ea6f19371786ed75630f91c67d5fdf 100644 (file)
@@ -225,7 +225,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         "roles" => %{"admin" => false, "moderator" => false},
         "tags" => [],
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-        "display_name" => HTML.strip_tags(user.name || user.nickname)
+        "display_name" => HTML.strip_tags(user.name || user.nickname),
+        "confirmation_pending" => false
       }
 
       assert expected == json_response(conn, 200)
@@ -634,7 +635,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => user.deactivated,
@@ -644,7 +646,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => false,
             "tags" => ["foo", "bar"],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user.name || user.nickname)
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -685,7 +688,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -709,7 +713,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -733,7 +738,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -757,7 +763,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -781,7 +788,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -805,7 +813,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -824,7 +833,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+                   "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -853,7 +863,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -880,7 +891,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user.name || user.nickname)
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => admin.deactivated,
@@ -890,7 +902,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => true,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -900,7 +913,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "roles" => %{"admin" => true, "moderator" => false},
             "tags" => [],
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
+            "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -929,7 +943,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => admin.local,
             "tags" => [],
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+            "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -939,7 +954,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => second_admin.local,
             "tags" => [],
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
+            "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -970,7 +986,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => moderator.local,
                    "tags" => [],
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
+                   "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -994,7 +1011,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => user1.local,
             "tags" => ["first"],
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user1.name || user1.nickname)
+            "display_name" => HTML.strip_tags(user1.name || user1.nickname),
+            "confirmation_pending" => false
           },
           %{
             "deactivated" => false,
@@ -1004,7 +1022,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "local" => user2.local,
             "tags" => ["second"],
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
-            "display_name" => HTML.strip_tags(user2.name || user2.nickname)
+            "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+            "confirmation_pending" => false
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1040,7 +1059,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => user.local,
                    "tags" => [],
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(user.name || user.nickname)
+                   "display_name" => HTML.strip_tags(user.name || user.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -1066,7 +1086,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "local" => true,
                    "tags" => [],
                    "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
-                   "display_name" => HTML.strip_tags(admin.name || admin.nickname)
+                   "display_name" => HTML.strip_tags(admin.name || admin.nickname),
+                   "confirmation_pending" => false
                  }
                ]
              }
@@ -1135,7 +1156,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "local" => true,
                "tags" => [],
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
-               "display_name" => HTML.strip_tags(user.name || user.nickname)
+               "display_name" => HTML.strip_tags(user.name || user.nickname),
+               "confirmation_pending" => false
              }
 
     log_entry = Repo.one(ModerationLog)
@@ -1902,6 +1924,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
       Pleroma.Config.put([:instance, :dynamic_configuration], true)
     end
 
+    @tag capture_log: true
     test "create new config setting in db", %{conn: conn} do
       conn =
         post(conn, "/api/pleroma/admin/config", %{
@@ -2840,6 +2863,105 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
     end
   end
+
+  describe "instances" do
+    test "GET /instances/:instance/statuses" do
+      admin = insert(:user, is_admin: true)
+      user = insert(:user, local: false, nickname: "archaeme@archae.me")
+      user2 = insert(:user, local: false, nickname: "test@test.com")
+      insert_pair(:note_activity, user: user)
+      insert(:note_activity, user: user2)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/archae.me/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 2
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/test.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 1
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/nonexistent.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 0
+    end
+  end
+
+  describe "PATCH /confirm_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it confirms emails of two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/confirm_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
+
+  describe "PATCH /resend_confirmation_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it resend emails for two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
 end
 
 # Needed for testing
index 519b56d6cb64255f00ba55e34baffe74ad783012..77cfce4fa8e10fbcc0f6730f069434e75c7cf3be 100644 (file)
@@ -103,6 +103,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert user["locked"] == true
     end
 
+    test "updates the user's allow_following_move", %{conn: conn} do
+      user = insert(:user)
+
+      assert user.allow_following_move == true
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> patch("/api/v1/accounts/update_credentials", %{allow_following_move: "false"})
+
+      assert refresh_record(user).allow_following_move == false
+      assert user = json_response(conn, 200)
+      assert user["pleroma"]["allow_following_move"] == false
+    end
+
     test "updates the user's default scope", %{conn: conn} do
       user = insert(:user)
 
index 8fc2d93005f5cda6ec224ff92eaf078771b19581..585cb8a9e95d54b00572a949530d61e8416b9b25 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.InternalFetchActor
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.OAuth.Token
 
@@ -118,6 +119,28 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       refute acc_one == acc_two
       assert acc_two == acc_three
     end
+
+    test "returns 404 when user is invisible", %{conn: conn} do
+      user = insert(:user, %{invisible: true})
+
+      resp =
+        conn
+        |> get("/api/v1/accounts/#{user.nickname}")
+        |> json_response(404)
+
+      assert %{"error" => "Can't find user"} = resp
+    end
+
+    test "returns 404 for internal.fetch actor", %{conn: conn} do
+      %User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
+
+      resp =
+        conn
+        |> get("/api/v1/accounts/internal.fetch")
+        |> json_response(404)
+
+      assert %{"error" => "Can't find user"} = resp
+    end
   end
 
   describe "user timelines" do
index 5d5b56c8ebef16adedefb4af1a4ce9f719bafed6..550689788a377285559347240fa4f84751ca608e 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
-  use Pleroma.Web.ConnCase, async: true
+  use Pleroma.Web.ConnCase
 
   alias Pleroma.Web.MastodonAPI.FilterView
 
index 9ad6a4fa75cb17b3a09d6300c6414b5c67dcf2cd..ae5fee2bcd3b38b8b86ef52b7f30c6385f6e4d6a 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
-  use Pleroma.Web.ConnCase, async: true
+  use Pleroma.Web.ConnCase
 
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
index af88841ed234c5f8ba87a49312a79abe302247fe..35aefb7dc08f4efbd0cadb0f75cc767c60e7166a 100644 (file)
@@ -102,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     privacy = user.default_scope
 
     assert %{
-             pleroma: %{notification_settings: ^notification_settings},
+             pleroma: %{notification_settings: ^notification_settings, allow_following_move: true},
              source: %{privacy: ^privacy}
            } = AccountView.render("show.json", %{user: user, for: user})
   end
@@ -350,7 +350,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       }
     }
 
-    assert expected == AccountView.render("show.json", %{user: user, for: other_user})
+    assert expected ==
+             AccountView.render("show.json", %{user: refresh_record(user), for: other_user})
   end
 
   test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
@@ -374,6 +375,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     refute result.display_name == "<marquee> username </marquee>"
   end
 
+  test "never display nil user follow counts" do
+    user = insert(:user, following_count: 0, follower_count: 0)
+    result = AccountView.render("show.json", %{user: user})
+
+    assert result.following_count == 0
+    assert result.followers_count == 0
+  end
+
   describe "hiding follows/following" do
     test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
       user =
index c9043a69ada032c61f404dcf7c96800c9c8ecdd5..26e1afc857d5fc8f2ae5f68f65ba3f9cdc4f5920 100644 (file)
@@ -107,4 +107,31 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
     assert [] ==
              NotificationView.render("index.json", %{notifications: [notification], for: followed})
   end
+
+  test "Move notification" do
+    %{ap_id: old_ap_id} = old_user = insert(:user)
+    %{ap_id: _new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+    follower = insert(:user)
+
+    User.follow(follower, old_user)
+    Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
+    Pleroma.Tests.ObanHelpers.perform_all()
+
+    old_user = refresh_record(old_user)
+    new_user = refresh_record(new_user)
+
+    [notification] = Notification.for_user(follower)
+
+    expected = %{
+      id: to_string(notification.id),
+      pleroma: %{is_seen: false},
+      type: "move",
+      account: AccountView.render("show.json", %{user: old_user, for: follower}),
+      target: AccountView.render("show.json", %{user: new_user, for: follower}),
+      created_at: Utils.to_masto_date(notification.inserted_at)
+    }
+
+    assert [expected] ==
+             NotificationView.render("index.json", %{notifications: [notification], for: follower})
+  end
 end
index 37b7b62f5aa21b359861f6105aa2c1e36ce0a6c4..50235dfef85583e9b0c62a8ee2dd23fdc6e047c8 100644 (file)
@@ -35,23 +35,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       assert redirected_to(conn) == "/notice/#{note_activity.id}"
     end
 
-    test "500s when user not found", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = User.get_cached_by_ap_id(note_activity.data["actor"])
-      User.invalidate_cache(user)
-      Pleroma.Repo.delete(user)
-      [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
-      url = "/objects/#{uuid}"
-
-      conn =
-        conn
-        |> put_req_header("accept", "application/xml")
-        |> get(url)
-
-      assert response(conn, 500) == ~S({"error":"Something went wrong"})
-    end
-
     test "404s on private objects", %{conn: conn} do
       note_activity = insert(:direct_note_activity)
       object = Object.normalize(note_activity)
@@ -82,21 +65,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       assert redirected_to(conn) == "/notice/#{note_activity.id}"
     end
 
-    test "505s when user not found", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
-      user = User.get_cached_by_ap_id(note_activity.data["actor"])
-      User.invalidate_cache(user)
-      Pleroma.Repo.delete(user)
-
-      conn =
-        conn
-        |> put_req_header("accept", "text/html")
-        |> get("/activities/#{uuid}")
-
-      assert response(conn, 500) == ~S({"error":"Something went wrong"})
-    end
-
     test "404s on private activities", %{conn: conn} do
       note_activity = insert(:direct_note_activity)
       [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
@@ -127,21 +95,28 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
   end
 
   describe "GET notice/2" do
-    test "gets a notice in xml format", %{conn: conn} do
+    test "redirects to a proper object URL when json requested and the object is local", %{
+      conn: conn
+    } do
       note_activity = insert(:note_activity)
+      expected_redirect_url = Object.normalize(note_activity).data["id"]
 
-      conn
-      |> get("/notice/#{note_activity.id}")
-      |> response(200)
+      redirect_url =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get("/notice/#{note_activity.id}")
+        |> redirected_to()
+
+      assert redirect_url == expected_redirect_url
     end
 
-    test "gets a notice in AS2 format", %{conn: conn} do
-      note_activity = insert(:note_activity)
+    test "returns a 404 on remote notice when json requested", %{conn: conn} do
+      note_activity = insert(:note_activity, local: false)
 
       conn
       |> put_req_header("accept", "application/activity+json")
       |> get("/notice/#{note_activity.id}")
-      |> json_response(200)
+      |> response(404)
     end
 
     test "500s when actor not found", %{conn: conn} do
@@ -157,32 +132,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
       assert response(conn, 500) == ~S({"error":"Something went wrong"})
     end
 
-    test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      url = "/notice/#{note_activity.id}"
-
-      conn =
-        conn
-        |> put_req_header("accept", "application/activity+json")
-        |> get(url)
-
-      assert json_response(conn, 200)
-
-      user = insert(:user)
-
-      {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
-      url = "/notice/#{like_activity.id}"
-
-      assert like_activity.data["type"] == "Like"
-
-      conn =
-        build_conn()
-        |> put_req_header("accept", "application/activity+json")
-        |> get(url)
-
-      assert response(conn, 404)
-    end
-
     test "render html for redirect for html format", %{conn: conn} do
       note_activity = insert(:note_activity)
 
index 80a7541b22f3a84bd731737f54ee0d7589e1e914..8265f18dd28e1939d186f5264204aae65d2deac7 100644 (file)
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.StreamerTest do
   alias Pleroma.Web.Streamer.StreamerSocket
   alias Pleroma.Web.Streamer.Worker
 
-  @moduletag needs_streamer: true
+  @moduletag needs_streamer: true, capture_log: true
   clear_config_all([:instance, :skip_thread_containment])
 
   describe "user streams" do