Merge remote-tracking branch 'origin/develop' into user-exports-api

This commit is contained in:
Mark Felder 2024-09-06 13:01:17 -04:00
commit ab6d15dcc0
28 changed files with 253 additions and 184 deletions

View file

View file

@ -0,0 +1 @@
Resolved edge case where the API can report you are following a user but the relationship is not fully established.

View file

@ -0,0 +1 @@
Add a rate limiter to the OAuth App creation endpoint and ensure registered apps are assigned to users.

View file

@ -1 +0,0 @@
Prevent OAuth App flow from creating duplicate entries

View file

@ -0,0 +1 @@
Adjust more Oban workers to enforce unique job constraints.

View file

@ -597,7 +597,8 @@ config :pleroma, Oban,
plugins: [{Oban.Plugins.Pruner, max_age: 900}],
crontab: [
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker},
{"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}
]
config :pleroma, Pleroma.Formatter,
@ -711,6 +712,7 @@ config :pleroma, :rate_limit,
timeline: {500, 3},
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25},
oauth_app_creation: {900_000, 5},
relations_actions: {10_000, 10},
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},

View file

@ -58,8 +58,12 @@ defmodule Pleroma.Object.Fetcher do
end
end
@typep fetcher_errors ::
:error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier
# Note: will create a Create activity, which we need internally at the moment.
@spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()}
@spec fetch_object_from_id(String.t(), list()) ::
{:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors()
def fetch_object_from_id(id, options \\ []) do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},

View file

@ -94,9 +94,6 @@ defmodule Pleroma.User.Backup do
else
true ->
{:error, "Backup is missing id. Please insert it into the Repo first."}
e ->
{:error, e}
end
end
@ -123,14 +120,13 @@ defmodule Pleroma.User.Backup do
end
defp permitted?(user) do
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)},
days = Config.get([__MODULE__, :limit_days]),
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days),
{_, true} <- {:diff, diff > days} do
true
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do
days = Config.get([__MODULE__, :limit_days])
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
diff > days
else
{:last, nil} -> true
{:diff, false} -> false
end
end
@ -171,6 +167,7 @@ defmodule Pleroma.User.Backup do
@spec export(User.t(), export_types(), export_formats()) :: binary()
def export(user, type, format \\ :csv)
def export(user, type, format) do
type = Atom.to_string(type)
mapping_fun = get_mapping_fun(type)

View file

@ -12,7 +12,7 @@ defmodule Pleroma.User.Import do
require Logger
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
@spec perform(atom(), User.t(), String.t()) :: :ok | {:error, any()}
def perform(:mute_import, %User{} = user, actor) do
with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor),
{_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)},
@ -49,7 +49,7 @@ defmodule Pleroma.User.Import do
defp handle_error(op, user_id, error) do
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
error
{:error, error}
end
def blocks_import(%User{} = user, [_ | _] = actors) do

View file

@ -22,22 +22,27 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)
defp config, do: Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()}
@type results :: {:ok, Activity.t() | Object.t(), keyword()}
@type errors :: {:error | :reject, any()}
# The Repo.transaction will wrap the result in an {:ok, _}
# and only returns an {:error, _} if the error encountered was related
# to the SQL transaction
@spec common_pipeline(map(), keyword()) :: results() | errors()
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} ->
side_effects().handle_after_transaction(meta)
{:ok, activity, meta}
{:ok, value} ->
value
{:ok, {:error, _} = error} ->
error
{:ok, {:reject, _} = error} ->
error
{:error, e} ->
{:error, e}
{:reject, e} ->
{:reject, e}
end
end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants
require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def block(blocked, blocker) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
@ -35,7 +35,7 @@ defmodule Pleroma.Web.CommonAPI do
end
@spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | Pipeline.errors()
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
@ -58,7 +58,7 @@ defmodule Pleroma.Web.CommonAPI do
)} do
{:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
{:common_pipeline, e} -> e
e -> e
end
end
@ -99,7 +99,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unblock(User.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}
def unblock(blocked, blocker) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
@ -120,7 +121,9 @@ defmodule Pleroma.Web.CommonAPI do
end
@spec follow(User.t(), User.t()) ::
{:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected}
{:ok, User.t(), User.t(), Activity.t() | Object.t()}
| {:error, :rejected}
| Pipeline.errors()
def follow(followed, follower) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -145,7 +148,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()
def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
@ -154,7 +157,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
@ -163,7 +166,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec delete(String.t(), User.t()) ::
{:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}
def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
@ -213,7 +217,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
@ -231,7 +235,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
@ -247,7 +251,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec favorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}
def favorite(id, %User{} = user) do
case favorite_helper(user, id) do
{:ok, _} = res ->
@ -285,7 +290,8 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
@spec unfavorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:error, :not_found | String.t()}
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
@ -302,7 +308,7 @@ defmodule Pleroma.Web.CommonAPI do
end
@spec react_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false),
@ -316,7 +322,7 @@ defmodule Pleroma.Web.CommonAPI do
end
@spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
{:ok, Activity.t()} | {:error, String.t()}
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
@ -329,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()}
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()
def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
@ -461,7 +467,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}
def update(orig_activity, %User{} = user, changes) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes),
@ -497,7 +503,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id),
@ -537,7 +543,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
@ -552,7 +558,7 @@ defmodule Pleroma.Web.CommonAPI do
end
end
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}
def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)

View file

@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :oauth_app_creation] when action == :create)
plug(:skip_auth when action in [:create, :verify_credentials])
plug(Pleroma.Web.ApiSpec.CastAndValidate)
@ -36,7 +38,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
|> Map.put(:scopes, scopes)
|> Maps.put_if_present(:user_id, user_id)
with {:ok, app} <- App.get_or_make(app_attrs) do
with cs <- App.register_changeset(%App{}, app_attrs),
{:ok, app} <- Repo.insert(cs) do
render(conn, "show.json", app: app)
end
end

View file

@ -92,14 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
User.get_follow_state(reading_user, target)
end
followed_by =
if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: :follow_accept} -> true
_ -> false
end
else
User.following?(target, reading_user)
followed_by = FollowingRelationship.following?(target, reading_user)
following = FollowingRelationship.following?(reading_user, target)
requested =
cond do
following -> false
true -> match?(:follow_pending, follow_state)
end
subscribing =
@ -114,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
following: follow_state == :follow_accept,
following: following,
followed_by: followed_by,
blocking:
UserRelationship.exists?(
@ -150,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
),
subscribing: subscribing,
notifying: subscribing,
requested: follow_state == :follow_pending,
requested: requested,
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
not UserRelationship.exists?(

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@type t :: %__MODULE__{}
@ -67,27 +68,35 @@ defmodule Pleroma.Web.OAuth.App do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
app
|> changeset(params)
|> validate_required([:scopes])
|> Repo.update()
end
end
@doc """
Gets app by attrs or create new with attrs.
Updates the attrs if needed.
Gets app by attrs or create new with attrs.
And updates the scopes if need.
"""
@spec get_or_make(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, client_name: attrs.client_name) do
__MODULE__.update(app.id, Map.take(attrs, [:scopes, :website]))
@spec get_or_make(map(), list(String.t())) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
update_scopes(app, scopes)
else
_e ->
%__MODULE__{}
|> register_changeset(attrs)
|> register_changeset(Map.put(attrs, :scopes, scopes))
|> Repo.insert()
end
end
defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app}
defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app}
defp update_scopes(%__MODULE__{} = app, scopes) do
app
|> change(%{scopes: scopes})
|> Repo.update()
end
@spec search(map()) :: {:ok, [t()], non_neg_integer()}
def search(params) do
query = from(a in __MODULE__)
@ -147,4 +156,29 @@ defmodule Pleroma.Web.OAuth.App do
Map.put(acc, key, error)
end)
end
@spec maybe_update_owner(Token.t()) :: :ok
def maybe_update_owner(%Token{app_id: app_id, user_id: user_id}) when not is_nil(user_id) do
__MODULE__.update(app_id, %{user_id: user_id})
:ok
end
def maybe_update_owner(_), do: :ok
@spec remove_orphans(pos_integer()) :: :ok
def remove_orphans(limit \\ 100) do
fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second)
Repo.transaction(fn ->
from(a in __MODULE__,
where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago,
limit: ^limit
)
|> Repo.all()
|> Enum.each(&Repo.delete(&1))
end)
:ok
end
end

View file

@ -318,6 +318,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
App.maybe_update_owner(token)
conn
|> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))

View file

@ -0,0 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.AppCleanupWorker do
@moduledoc """
Cleans up registered apps that were never associated with a user.
"""
use Oban.Worker, queue: "background"
alias Pleroma.Web.OAuth.App
@impl true
def perform(_job) do
App.remove_orphans()
end
@impl true
def timeout(_job), do: :timer.seconds(30)
end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.User
alias Pleroma.Web.Federator
use Oban.Worker, queue: :federator_incoming, max_attempts: 5
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Workers.RemoteFetcherWorker do
alias Pleroma.Object.Fetcher
use Oban.Worker, queue: :background
use Oban.Worker, queue: :background, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.RichMediaWorker do
alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Card
use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300]
use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.UserRefreshWorker do
use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300]
use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity]
alias Pleroma.User

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.WebPusherWorker do
alias Pleroma.Repo
alias Pleroma.Web.Push.Impl
use Oban.Worker, queue: :web_push
use Oban.Worker, queue: :web_push, unique: [period: :infinity]
@impl true
def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do

View file

@ -22,7 +22,7 @@
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},

View file

@ -0,0 +1,21 @@
defmodule Pleroma.Repo.Migrations.AssignAppUser do
use Ecto.Migration
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
def up do
Repo.all(Token)
|> Enum.group_by(fn x -> Map.get(x, :app_id) end)
|> Enum.each(fn {_app_id, tokens} ->
token =
Enum.filter(tokens, fn x -> not is_nil(x.user_id) end)
|> List.first()
App.maybe_update_owner(token)
end)
end
def down, do: :ok
end

View file

@ -41,6 +41,10 @@ defmodule Pleroma.HTMLTest do
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
"""
@mention_hashtags_sample """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
describe "StripTags scrubber" do
test "works as expected" do
expected = """
@ -126,6 +130,15 @@ defmodule Pleroma.HTMLTest do
Pleroma.HTML.Scrubber.TwitterText
)
end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end
describe "default scrubber" do
@ -189,6 +202,15 @@ defmodule Pleroma.HTMLTest do
Pleroma.HTML.Scrubber.Default
)
end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end
describe "extract_first_external_url_from_object" do

View file

@ -89,114 +89,4 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
assert expected == json_response_and_validate_schema(conn, 200)
assert app.user_id == user.id
end
test "creates an oauth app without a user", %{conn: conn} do
app_attrs = build(:oauth_app)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: app_attrs.client_name,
redirect_uris: app_attrs.redirect_uris
})
[app] = Repo.all(App)
expected = %{
"name" => app.client_name,
"website" => app.website,
"client_id" => app.client_id,
"client_secret" => app.client_secret,
"id" => app.id |> to_string(),
"redirect_uri" => app.redirect_uris,
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
}
assert expected == json_response_and_validate_schema(conn, 200)
end
test "does not duplicate apps with the same client name", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
for _i <- 1..3 do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris
})
|> json_response_and_validate_schema(200)
end
apps = Repo.all(App)
assert length(apps) == 1
assert List.first(apps).client_name == client_name
assert List.first(apps).redirect_uris == redirect_uris
end
test "app scopes can be updated", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
website = "https://bleromase.com"
scopes = "read write"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website,
scopes: scopes
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).scopes == String.split(scopes, " ")
updated_scopes = "read write push"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website,
scopes: updated_scopes
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).scopes == String.split(updated_scopes, " ")
end
test "app website URL can be updated", %{conn: conn} do
client_name = "BleromaSE"
redirect_uris = "https://bleroma.app/oauth-callback"
website = "https://bleromase.com"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: website
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).website == website
updated_website = "https://bleromase2ultimateedition.com"
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: client_name,
redirect_uris: redirect_uris,
website: updated_website
})
|> json_response_and_validate_schema(200)
assert List.first(Repo.all(App)).website == updated_website
end
end

View file

@ -456,6 +456,45 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test_relationship_rendering(user, other_user, expected)
end
test "relationship does not indicate following if a FollowingRelationship is missing" do
user = insert(:user)
other_user = insert(:user, local: false)
# Create a follow relationship with the real Follow Activity and Accept it
assert {:ok, _, _, _} = CommonAPI.follow(other_user, user)
assert {:ok, _} = CommonAPI.accept_follow_request(user, other_user)
assert %{data: %{"state" => "accept"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, other_user)
# Fetch the relationship and forcibly delete it to simulate
# a Follow Accept that did not complete processing
%{following_relationships: [relationship]} =
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
assert {:ok, _} = Pleroma.Repo.delete(relationship)
assert %{following_relationships: [], user_relationships: []} ==
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
expected =
Map.merge(
@blank_response,
%{
following: false,
followed_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
notifying: false,
showing_reblogs: true,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the blocking and blocked user" do
user = insert(:user)
other_user = insert(:user)

View file

@ -12,23 +12,20 @@ defmodule Pleroma.Web.OAuth.AppTest do
test "gets exist app" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
{:ok, %App{} = exist_app} = App.get_or_make(attrs)
{:ok, %App{} = exist_app} = App.get_or_make(attrs, [])
assert exist_app == app
end
test "make app" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["write"]}
{:ok, %App{} = app} = App.get_or_make(attrs)
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
assert app.scopes == ["write"]
end
test "gets exist app and updates scopes" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: ["read", "write"]}
app = insert(:oauth_app, attrs)
{:ok, %App{} = exist_app} =
App.get_or_make(%{attrs | scopes: ["read", "write", "follow", "push"]})
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
app = insert(:oauth_app, Map.merge(attrs, %{scopes: ["read", "write"]}))
{:ok, %App{} = exist_app} = App.get_or_make(attrs, ["read", "write", "follow", "push"])
assert exist_app.id == app.id
assert exist_app.scopes == ["read", "write", "follow", "push"]
end
@ -56,4 +53,21 @@ defmodule Pleroma.Web.OAuth.AppTest do
assert Enum.sort(App.get_user_apps(user)) == Enum.sort(apps)
end
test "removes orphaned apps" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
{:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"])
attrs = %{client_name: "PleromaFE", redirect_uris: "."}
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
# backdate the old app so it's within the threshold for being cleaned up
{:ok, _} =
"UPDATE apps SET inserted_at = now() - interval '1 hour' WHERE id = #{old_app.id}"
|> Pleroma.Repo.query()
App.remove_orphans()
assert [app] == Pleroma.Repo.all(App)
end
end

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
@ -770,6 +771,9 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
# Verify app has no associated user yet
assert %Pleroma.Web.OAuth.App{user_id: nil} = Repo.get_by(App, %{id: app.id})
conn =
build_conn()
|> post("/oauth/token", %{
@ -786,6 +790,10 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert token
assert token.scopes == auth.scopes
assert user.ap_id == ap_id
# Verify app has an associated user now
user_id = user.id
assert %Pleroma.Web.OAuth.App{user_id: ^user_id} = Repo.get_by(App, %{id: app.id})
end
test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do