소스 검색

Merge remote-tracking branch 'upstream/develop' into oauth-token-id

feature/config-versioning
Alex Gleason 3 년 전
부모
커밋
e7ac15905e
No known key found for this signature in database GPG 키 ID: 7211D1F99744FBB7
81개의 변경된 파일2057개의 추가작업 그리고 323개의 파일을 삭제
  1. +6
    -4
      .gitlab-ci.yml
  2. +11
    -0
      CHANGELOG.md
  3. +2
    -0
      config/config.exs
  4. +17
    -0
      config/description.exs
  5. +6
    -0
      docs/configuration/cheatsheet.md
  6. +1
    -0
      docs/development/API/differences_in_mastoapi_responses.md
  7. +1
    -1
      docs/index.md
  8. +1
    -1
      docs/installation/otp_en.md
  9. +53
    -37
      lib/pleroma/activity.ex
  10. +5
    -0
      lib/pleroma/activity/queries.ex
  11. +10
    -8
      lib/pleroma/config/release_runtime_provider.ex
  12. +1
    -1
      lib/pleroma/config_db.ex
  13. +8
    -0
      lib/pleroma/object/containment.ex
  14. +41
    -36
      lib/pleroma/user.ex
  15. +19
    -0
      lib/pleroma/utils.ex
  16. +56
    -4
      lib/pleroma/web/activity_pub/activity_pub.ex
  17. +8
    -0
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  18. +32
    -0
      lib/pleroma/web/activity_pub/builder.ex
  19. +59
    -0
      lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex
  20. +54
    -114
      lib/pleroma/web/activity_pub/object_validator.ex
  21. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
  22. +77
    -0
      lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex
  23. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
  24. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/answer_validator.ex
  25. +5
    -4
      lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex
  26. +1
    -2
      lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
  27. +5
    -4
      lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex
  28. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/block_validator.ex
  29. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
  30. +8
    -0
      lib/pleroma/web/activity_pub/object_validators/common_validations.ex
  31. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
  32. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
  33. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
  34. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
  35. +5
    -4
      lib/pleroma/web/activity_pub/object_validators/event_validator.ex
  36. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
  37. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/like_validator.ex
  38. +5
    -4
      lib/pleroma/web/activity_pub/object_validators/question_validator.ex
  39. +77
    -0
      lib/pleroma/web/activity_pub/object_validators/tag_validator.ex
  40. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/undo_validator.ex
  41. +1
    -1
      lib/pleroma/web/activity_pub/object_validators/update_validator.ex
  42. +12
    -13
      lib/pleroma/web/activity_pub/pipeline.ex
  43. +59
    -2
      lib/pleroma/web/activity_pub/side_effects.ex
  44. +3
    -2
      lib/pleroma/web/activity_pub/transmogrifier.ex
  45. +21
    -0
      lib/pleroma/web/activity_pub/views/user_view.ex
  46. +44
    -2
      lib/pleroma/web/api_spec/operations/status_operation.ex
  47. +7
    -0
      lib/pleroma/web/api_spec/schemas/status.ex
  48. +46
    -27
      lib/pleroma/web/common_api.ex
  49. +6
    -0
      lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
  50. +12
    -0
      lib/pleroma/web/mastodon_api/controllers/status_controller.ex
  51. +2
    -1
      lib/pleroma/web/mastodon_api/views/instance_view.ex
  52. +17
    -6
      lib/pleroma/web/mastodon_api/views/status_view.ex
  53. +2
    -1
      lib/pleroma/web/plugs/http_security_plug.ex
  54. +1
    -0
      lib/pleroma/web/router.ex
  55. +1
    -1
      mix.exs
  56. +9
    -0
      priv/repo/migrations/20210202110641_add_pinned_objects_to_users.exs
  57. +23
    -0
      priv/repo/migrations/20210203141144_add_featured_address_to_users.exs
  58. +28
    -0
      priv/repo/migrations/20210205145000_move_pinned_activities_into_pinned_objects.exs
  59. +15
    -0
      priv/repo/migrations/20210206045221_remove_pinned_activities_from_users.exs
  60. +17
    -0
      priv/repo/migrations/20210401143153_user_notification_settings_fix.exs
  61. +5
    -0
      test/fixtures/config/temp.exported_from_db.secret.exs
  62. +39
    -0
      test/fixtures/mastodon/collections/featured.json
  63. +47
    -0
      test/fixtures/statuses/masto-note.json
  64. +27
    -0
      test/fixtures/statuses/note.json
  65. +18
    -0
      test/fixtures/users_mock/masto_featured.json
  66. +42
    -0
      test/fixtures/users_mock/user.json
  67. +22
    -0
      test/pleroma/activity_test.exs
  68. +45
    -0
      test/pleroma/config/release_runtime_provider_test.exs
  69. +45
    -0
      test/pleroma/user_test.exs
  70. +205
    -0
      test/pleroma/web/activity_pub/activity_pub_controller_test.exs
  71. +77
    -0
      test/pleroma/web/activity_pub/activity_pub_test.exs
  72. +126
    -0
      test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs
  73. +16
    -7
      test/pleroma/web/activity_pub/pipeline_test.exs
  74. +172
    -0
      test/pleroma/web/activity_pub/transmogrifier/add_remove_handling_test.exs
  75. +76
    -0
      test/pleroma/web/admin_api/controllers/config_controller_test.exs
  76. +51
    -9
      test/pleroma/web/common_api_test.exs
  77. +27
    -11
      test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
  78. +2
    -1
      test/pleroma/web/mastodon_api/views/status_view_test.exs
  79. +30
    -0
      test/pleroma/web/twitter_api/remote_follow_controller_test.exs
  80. +49
    -3
      test/support/factory.ex
  81. +24
    -0
      test/support/http_request_mock.ex

+ 6
- 4
.gitlab-ci.yml 파일 보기

@@ -8,7 +8,9 @@ variables: &global_variables
MIX_ENV: test

cache: &global_cache_policy
key: ${CI_COMMIT_REF_SLUG}
key:
files:
- mix.lock
paths:
- deps
- _build
@@ -171,8 +173,8 @@ spec-deploy:
- apk add curl
script:
- curl -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline
stop_review_app:
image: alpine:3.9
stage: deploy
@@ -231,7 +233,7 @@ amd64-musl:
stage: release
artifacts: *release-artifacts
only: *release-only
image: elixir:1.10.3-alpine
image: elixir:1.10.3-alpine
cache: *release-cache
variables: *release-variables
before_script: &before-release-musl


+ 11
- 0
CHANGELOG.md 파일 보기

@@ -6,13 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Changed

- The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
- HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.

### Added

- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.

## Unreleased (Patch)

### Fixed

- Try to save exported ConfigDB settings (migrate_from_db) in the system temp directory if default location is not writable.
- Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image
- Applying ConcurrentLimiter settings via AdminAPI
- User login failures if their `notification_settings` were in a NULL state.
- Mix task `pleroma.user delete_activities` query transaction timeout is now :infinity

## [2.3.0] - 2020-03-01



+ 2
- 0
config/config.exs 파일 보기

@@ -409,6 +409,8 @@ config :pleroma, :mrf_object_age,
threshold: 604_800,
actions: [:delist, :strip_followers]

config :pleroma, :mrf_follow_bot, follower_nickname: nil

config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],


+ 17
- 0
config/description.exs 파일 보기

@@ -2944,6 +2944,23 @@ config :pleroma, :config_description, [
},
%{
group: :pleroma,
key: :mrf_follow_bot,
tab: :mrf,
related_policy: "Pleroma.Web.ActivityPub.MRF.FollowBotPolicy",
label: "MRF FollowBot Policy",
type: :group,
description: "Automatically follows newly discovered accounts.",
children: [
%{
key: :follower_nickname,
type: :string,
description: "The name of the bot account to use for following newly discovered users.",
suggestions: ["followbot"]
}
]
},
%{
group: :pleroma,
key: :modules,
type: :group,
description: "Custom Runtime Modules",


+ 6
- 0
docs/configuration/cheatsheet.md 파일 보기

@@ -124,6 +124,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
* `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
* `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.

@@ -220,6 +221,11 @@ Notes:
- The hashtags in the configuration do not have a leading `#`.
- This MRF Policy is always enabled, if you want to disable it you have to set empty lists

#### :mrf_follow_bot

* `follower_nickname`: The name of the bot account to use for following newly discovered users. Using `followbot` or similar is strongly suggested.


### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances


+ 1
- 0
docs/development/API/differences_in_mastoapi_responses.md 파일 보기

@@ -38,6 +38,7 @@ Has these additional fields under the `pleroma` object:
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not.
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.

## Scheduled statuses



+ 1
- 1
docs/index.md 파일 보기

@@ -20,7 +20,7 @@ The default front-end used by Pleroma is Pleroma-FE. You can find more informati

### Mastodon interface
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
Just add a "/web" after your instance url (e.g. <https://pleroma.soycaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
Just add a "/web" after your instance url (e.g. <https://pleroma.soykaf.com/web>) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC!
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.

Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.

+ 1
- 1
docs/installation/otp_en.md 파일 보기

@@ -290,7 +290,7 @@ nginx -t

## Create your first user and set as admin
```sh
cd /opt/pleroma/bin
cd /opt/pleroma
su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin"
```
This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password.


+ 53
- 37
lib/pleroma/activity.ex 파일 보기

@@ -184,40 +184,48 @@ defmodule Pleroma.Activity do
|> Repo.one()
end

@spec get_by_id(String.t()) :: Activity.t() | nil
def get_by_id(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()

_ ->
nil
end
end

def get_by_id_with_user_actor(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> with_preloaded_user_actor()
|> Repo.one()

_ ->
nil
@doc """
Gets activity by ID, doesn't load activities from deactivated actors by default.
"""
@spec get_by_id(String.t(), keyword()) :: t() | nil
def get_by_id(id, opts \\ [filter: [:restrict_deactivated]]), do: get_by_id_with_opts(id, opts)

@spec get_by_id_with_user_actor(String.t()) :: t() | nil
def get_by_id_with_user_actor(id), do: get_by_id_with_opts(id, preload: [:user_actor])

@spec get_by_id_with_object(String.t()) :: t() | nil
def get_by_id_with_object(id), do: get_by_id_with_opts(id, preload: [:object])

defp get_by_id_with_opts(id, opts) do
if FlakeId.flake_id?(id) do
query = Queries.by_id(id)

with_filters_query =
if is_list(opts[:filter]) do
Enum.reduce(opts[:filter], query, fn
{:type, type}, acc -> Queries.by_type(acc, type)
:restrict_deactivated, acc -> restrict_deactivated_users(acc)
_, acc -> acc
end)
else
query
end

with_preloads_query =
if is_list(opts[:preload]) do
Enum.reduce(opts[:preload], with_filters_query, fn
:user_actor, acc -> with_preloaded_user_actor(acc)
:object, acc -> with_preloaded_object(acc)
_, acc -> acc
end)
else
with_filters_query
end

Repo.one(with_preloads_query)
end
end

def get_by_id_with_object(id) do
Activity
|> where(id: ^id)
|> with_preloaded_object()
|> Repo.one()
end

def all_by_ids_with_object(ids) do
Activity
|> where([a], a.id in ^ids)
@@ -269,6 +277,11 @@ defmodule Pleroma.Activity do

def get_create_by_object_ap_id_with_object(_), do: nil

@spec create_by_id_with_object(String.t()) :: t() | nil
def create_by_id_with_object(id) do
get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"])
end

defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
get_create_by_object_ap_id_with_object(ap_id)
end
@@ -368,12 +381,6 @@ defmodule Pleroma.Activity do
end
end

@spec pinned_by_actor?(Activity.t()) :: boolean()
def pinned_by_actor?(%Activity{} = activity) do
actor = user_actor(activity)
activity.id in actor.pinned_activities
end

@spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
ap_id
@@ -384,4 +391,13 @@ defmodule Pleroma.Activity do
end

def get_by_object_ap_id_with_object(_), do: nil

@spec add_by_params_query(String.t(), String.t(), String.t()) :: Ecto.Query.t()
def add_by_params_query(object_id, actor, target) do
object_id
|> Queries.by_object_id()
|> Queries.by_type("Add")
|> Queries.by_actor(actor)
|> where([a], fragment("?->>'target' = ?", a.data, ^target))
end
end

+ 5
- 0
lib/pleroma/activity/queries.ex 파일 보기

@@ -14,6 +14,11 @@ defmodule Pleroma.Activity.Queries do
alias Pleroma.Activity
alias Pleroma.User

@spec by_id(query(), String.t()) :: query()
def by_id(query \\ Activity, id) do
from(a in query, where: a.id == ^id)
end

@spec by_ap_id(query, String.t()) :: query
def by_ap_id(query \\ Activity, ap_id) do
from(


+ 10
- 8
lib/pleroma/config/release_runtime_provider.ex 파일 보기

@@ -1,6 +1,6 @@
defmodule Pleroma.Config.ReleaseRuntimeProvider do
@moduledoc """
Imports `runtime.exs` and `{env}.exported_from_db.secret.exs` for elixir releases.
Imports runtime config and `{env}.exported_from_db.secret.exs` for releases.
"""
@behaviour Config.Provider

@@ -8,10 +8,11 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
def init(opts), do: opts

@impl true
def load(config, _opts) do
def load(config, opts) do
with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults())

config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
config_path =
opts[:config_path] || System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"

with_runtime_config =
if File.exists?(config_path) do
@@ -24,7 +25,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
warning = [
IO.ANSI.red(),
IO.ANSI.bright(),
"!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
"!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
IO.ANSI.reset()
]

@@ -33,13 +34,14 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
end

exported_config_path =
config_path
|> Path.dirname()
|> Path.join("prod.exported_from_db.secret.exs")
opts[:exported_config_path] ||
config_path
|> Path.dirname()
|> Path.join("#{Pleroma.Config.get(:env)}.exported_from_db.secret.exs")

with_exported =
if File.exists?(exported_config_path) do
exported_config = Config.Reader.read!(with_runtime_config)
exported_config = Config.Reader.read!(exported_config_path)
Config.Reader.merge(with_runtime_config, exported_config)
else
with_runtime_config


+ 1
- 1
lib/pleroma/config_db.ex 파일 보기

@@ -387,6 +387,6 @@ defmodule Pleroma.ConfigDB do
@spec module_name?(String.t()) :: boolean()
def module_name?(string) do
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
string in ["Oban", "Ueberauth", "ExSyslogger"]
string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
end
end

+ 8
- 0
lib/pleroma/object/containment.ex 파일 보기

@@ -71,6 +71,14 @@ defmodule Pleroma.Object.Containment do
compare_uris(id_uri, other_uri)
end

# Mastodon pin activities don't have an id, so we check the object field, which will be pinned.
def contain_origin_from_id(id, %{"object" => object}) when is_binary(object) do
id_uri = URI.parse(id)
object_uri = URI.parse(object)

compare_uris(id_uri, object_uri)
end

def contain_origin_from_id(_id, _data), do: :error

def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),


+ 41
- 36
lib/pleroma/user.ex 파일 보기

@@ -99,6 +99,7 @@ defmodule Pleroma.User do
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:featured_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
@@ -130,7 +131,6 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
@@ -148,6 +148,7 @@ defmodule Pleroma.User do
field(:accepts_chat_messages, :boolean, default: nil)
field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{})

embeds_one(
:notification_settings,
@@ -372,8 +373,10 @@ defmodule Pleroma.User do
end

# Should probably be renamed or removed
@spec ap_id(User.t()) :: String.t()
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"

@spec ap_followers(User.t()) :: String.t()
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"

@@ -381,6 +384,11 @@ 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"

@spec ap_featured_collection(User.t()) :: String.t()
def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa

def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"

defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
@@ -443,6 +451,7 @@ defmodule Pleroma.User do
:uri,
:follower_address,
:following_address,
:featured_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
@@ -454,7 +463,8 @@ defmodule Pleroma.User do
:invisible,
:actor_type,
:also_known_as,
:accepts_chat_messages
:accepts_chat_messages,
:pinned_objects
]
)
|> cast(params, [:name], empty_values: [])
@@ -686,7 +696,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
|> put_following_and_follower_and_featured_address()
end

def register_changeset(struct, params \\ %{}, opts \\ []) do
@@ -747,7 +757,7 @@ defmodule Pleroma.User do
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
|> put_following_and_follower_and_featured_address()
end

def maybe_validate_required_email(changeset, true), do: changeset
@@ -765,11 +775,16 @@ defmodule Pleroma.User do
put_change(changeset, :ap_id, ap_id)
end

defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
defp put_following_and_follower_and_featured_address(changeset) do
user = %User{nickname: get_field(changeset, :nickname)}
followers = ap_followers(user)
following = ap_following(user)
featured = ap_featured_collection(user)

changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
|> put_change(:featured_address, featured)
end

defp autofollow_users(user) do
@@ -2343,45 +2358,35 @@ defmodule Pleroma.User do
cast(user, %{is_approved: approved?}, [:is_approved])
end

def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}

# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
Oban.cancel_job(expiration.id)
end
@spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
def add_pinned_object_id(%User{} = user, object_id) do
if !user.pinned_objects[object_id] do
params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}

user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
max: max_pinned_statuses,
message: "You have already pinned the maximum number of statuses"
)
|> cast(params, [:pinned_objects])
|> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)

if Enum.count(pinned_objects) <= max_pinned_statuses do
[]
else
[pinned_objects: "You have already pinned the maximum number of statuses"]
end
end)
else
change(user)
end
|> update_and_set_cache()
end

def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}

# if pinned activity was scheduled for deletion, we reschedule it for deletion
if data["expires_at"] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()

Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: id,
expires_at: expires_at
})
end

@spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
def remove_pinned_object_id(%User{} = user, object_id) do
user
|> cast(params, [:pinned_activities])
|> cast(
%{pinned_objects: Map.delete(user.pinned_objects, object_id)},
[:pinned_objects]
)
|> update_and_set_cache()
end



+ 19
- 0
lib/pleroma/utils.ex 파일 보기

@@ -11,6 +11,8 @@ defmodule Pleroma.Utils do
eperm epipe erange erofs espipe esrch estale etxtbsy exdev
)a

@repo_timeout Pleroma.Config.get([Pleroma.Repo, :timeout], 15_000)

def compile_dir(dir) when is_binary(dir) do
dir
|> File.ls!()
@@ -63,4 +65,21 @@ defmodule Pleroma.Utils do
end

def posix_error_message(_), do: ""

@doc """
Returns [timeout: integer] suitable for passing as an option to Repo functions.

This function detects if the execution was triggered from IEx shell, Mix task, or
./bin/pleroma_ctl and sets the timeout to :infinity, else returns the default timeout value.
"""
@spec query_timeout() :: [timeout: integer]
def query_timeout do
{parent, _, _, _} = Process.info(self(), :current_stacktrace) |> elem(1) |> Enum.fetch!(2)

cond do
parent |> to_string |> String.starts_with?("Elixir.Mix.Task") -> [timeout: :infinity]
parent == :erl_eval -> [timeout: :infinity]
true -> [timeout: @repo_timeout]
end
end
end

+ 56
- 4
lib/pleroma/web/activity_pub/activity_pub.ex 파일 보기

@@ -630,7 +630,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:user, reading_user)
|> Map.put(:actor_id, user.ap_id)
|> Map.put(:pinned_activity_ids, user.pinned_activities)
|> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))

params =
if User.blocks?(reading_user, user) do
@@ -1075,8 +1075,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_unlisted(query, _), do: query

defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do
from(activity in query, where: activity.id in ^ids)
defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do
from(
[activity, object: o] in query,
where:
fragment(
"(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)",
activity.data,
activity.data,
activity.data,
^ids
)
)
end

defp restrict_pinned(query, _), do: query
@@ -1419,6 +1429,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"

featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)

public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
@@ -1447,13 +1460,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages
accepts_chat_messages: accepts_chat_messages,
pinned_objects: pinned_objects
}

# nickname can be nil because of virtual actors
@@ -1591,6 +1606,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

def pin_data_from_featured_collection(%{
"type" => type,
"orderedItems" => objects
})
when type in ["OrderedCollection", "Collection"] do
Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end)
end

def fetch_and_prepare_featured_from_ap_id(nil) do
{:ok, %{}}
end

def fetch_and_prepare_featured_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
{:ok, pin_data_from_featured_collection(data)}
else
e ->
Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}")
{:ok, %{}}
end
end

def pinned_fetch_task(nil), do: nil

def pinned_fetch_task(%{pinned_objects: pins}) do
if Enum.all?(pins, fn {ap_id, _} ->
Object.get_cached_by_ap_id(ap_id) ||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
end) do
:ok
else
:error
end
end

def make_user_from_ap_id(ap_id) do
user = User.get_cached_by_ap_id(ap_id)

@@ -1598,6 +1648,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)

if user do
user
|> User.remote_user_changeset(data)


+ 8
- 0
lib/pleroma/web/activity_pub/activity_pub_controller.ex 파일 보기

@@ -543,4 +543,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(object.data)
end
end

def pinned(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("featured.json", %{user: user}))
end
end
end

+ 32
- 0
lib/pleroma/web/activity_pub/builder.ex 파일 보기

@@ -273,4 +273,36 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"context" => object.data["context"]
}, []}
end

@spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
def pin(%User{} = user, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"target" => pinned_url(user.nickname),
"object" => object.data["id"],
"actor" => user.ap_id,
"type" => "Add",
"to" => [Pleroma.Constants.as_public()],
"cc" => [user.follower_address]
}, []}
end

@spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
def unpin(%User{} = user, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"target" => pinned_url(user.nickname),
"object" => object.data["id"],
"actor" => user.ap_id,
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()],
"cc" => [user.follower_address]
}, []}
end

defp pinned_url(nickname) when is_binary(nickname) do
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
end
end

+ 59
- 0
lib/pleroma/web/activity_pub/mrf/follow_bot_policy.ex 파일 보기

@@ -0,0 +1,59 @@
defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI

require Logger

@impl true
def filter(message) do
with follower_nickname <- Config.get([:mrf_follow_bot, :follower_nickname]),
%User{actor_type: "Service"} = follower <-
User.get_cached_by_nickname(follower_nickname),
%{"type" => "Create", "object" => %{"type" => "Note"}} <- message do
try_follow(follower, message)
else
nil ->
Logger.warn(
"#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname
account does not exist, or the account is not correctly configured as a bot."
)

{:ok, message}

_ ->
{:ok, message}
end
end

defp try_follow(follower, message) do
to = Map.get(message, "to", [])
cc = Map.get(message, "cc", [])
actor = [message["actor"]]

Enum.concat([to, cc, actor])
|> List.flatten()
|> Enum.uniq()
|> User.get_all_by_ap_id()
|> Enum.each(fn user ->
with false <- user.local,
false <- User.following?(follower, user),
false <- User.locked?(user),
false <- (user.bio || "") |> String.downcase() |> String.contains?("nobot") do
Logger.debug(
"#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}"
)

CommonAPI.follow(follower, user)
end
end)

{:ok, message}
end

@impl true
def describe do
{:ok, %{}}
end
end

+ 54
- 114
lib/pleroma/web/activity_pub/object_validator.ex 파일 보기

@@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator
@@ -37,37 +38,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@impl true
def validate(object, meta)

def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject] do
with {:ok, object} <-
object
|> AcceptRejectValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Event"} = object, meta) do
with {:ok, object} <-
object
|> EventValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Follow"} = object, meta) do
with {:ok, object} <-
object
|> FollowValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Block"} = block_activity, meta) do
with {:ok, block_activity} <-
block_activity
@@ -87,16 +57,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end

def validate(%{"type" => "Update"} = update_activity, meta) do
with {:ok, update_activity} <-
update_activity
|> UpdateValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
update_activity = stringify_keys(update_activity)
{:ok, update_activity, meta}
end
end

def validate(%{"type" => "Undo"} = object, meta) do
with {:ok, object} <-
object
@@ -123,76 +83,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end

def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object
|> LikeValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "ChatMessage"} = object, meta) do
with {:ok, object} <-
object
|> ChatMessageValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Question"} = object, meta) do
with {:ok, object} <-
object
|> QuestionValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do
with {:ok, object} <-
object
|> AudioVideoValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Article"} = object, meta) do
with {:ok, object} <-
object
|> ArticleNoteValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "Answer"} = object, meta) do
with {:ok, object} <-
object
|> AnswerValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <-
object
|> EmojiReactValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(
%{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,
meta
@@ -224,10 +114,60 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end

def validate(%{"type" => "Announce"} = object, meta) do
def validate(%{"type" => type} = object, meta)
when type in ~w[Event Question Audio Video Article] do
validator =
case type do
"Event" -> EventValidator
"Question" -> QuestionValidator
"Audio" -> AudioVideoValidator
"Video" -> AudioVideoValidator
"Article" -> ArticleNoteValidator
end

with {:ok, object} <-
object
|> validator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)

# Insert copy of hashtags as strings for the non-hashtag table indexing
tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
object = Map.put(object, "tag", tag)

{:ok, object, meta}
end
end

def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer] do
validator =
case type do
"Accept" -> AcceptRejectValidator
"Reject" -> AcceptRejectValidator
"Follow" -> FollowValidator
"Update" -> UpdateValidator
"Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator
"ChatMessage" -> ChatMessageValidator
"Answer" -> AnswerValidator
end

with {:ok, object} <-
object
|> validator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end

def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do
with {:ok, object} <-
object
|> AnnounceValidator.cast_and_validate()
|> AddRemoveValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
@@ -260,7 +200,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do

def cast_and_apply(o), do: {:error, {:validator_not_set, o}}

# is_struct/1 isn't present in Elixir 1.8.x
# is_struct/1 appears in Elixir 1.11
def stringify_keys(%{__struct__: _} = object) do
object
|> Map.from_struct()


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex 파일 보기

@@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Accept", "Reject"])


+ 77
- 0
lib/pleroma/web/activity_pub/object_validators/add_remove_validator.ex 파일 보기

@@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
use Ecto.Schema

import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations

require Pleroma.Constants

alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User

@primary_key false

embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:target)
field(:object, ObjectValidators.ObjectID)
field(:actor, ObjectValidators.ObjectID)
field(:type)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
end

def cast_and_validate(data) do
{:ok, actor} = User.get_or_fetch_by_ap_id(data["actor"])

{:ok, actor} = maybe_refetch_user(actor)

data
|> maybe_fix_data_for_mastodon(actor)
|> cast_data()
|> validate_data(actor)
end

defp maybe_fix_data_for_mastodon(data, actor) do
# Mastodon sends pin/unpin objects without id, to, cc fields
data
|> Map.put_new("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id())
|> Map.put_new("to", [Pleroma.Constants.as_public()])
|> Map.put_new("cc", [actor.follower_address])
end

defp cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end

defp validate_data(changeset, actor) do
changeset
|> validate_required([:id, :target, :object, :actor, :type, :to, :cc])
|> validate_inclusion(:type, ~w(Add Remove))
|> validate_actor_presence()
|> validate_collection_belongs_to_actor(actor)
|> validate_object_presence()
end

defp validate_collection_belongs_to_actor(changeset, actor) do
validate_change(changeset, :target, fn :target, target ->
if target == actor.featured_address do
[]
else
[target: "collection doesn't belong to actor"]
end
end)
end

defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary(address) do
{:ok, user}
end

defp maybe_refetch_user(%User{ap_id: ap_id}) do
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id)
end
end

+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/announce_validator.ex 파일 보기

@@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
cng
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Announce"])
|> validate_required([:id, :type, :object, :actor, :to, :cc])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/answer_validator.ex 파일 보기

@@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Answer"])
|> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])


+ 5
- 4
lib/pleroma/web/activity_pub/object_validators/article_note_validator.ex 파일 보기

@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier

import Ecto.Changeset
@@ -22,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
embeds_many(:tag, TagValidator)
field(:type, :string)

field(:name, :string)
@@ -90,11 +90,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
data = fix(data)

struct
|> cast(data, __schema__(:fields) -- [:attachment])
|> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment)
|> cast_embed(:tag)
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Article", "Note"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])


+ 1
- 2
lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex 파일 보기

@@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
use Ecto.Schema

alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator

import Ecto.Changeset

@@ -90,7 +89,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
end
end

def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_inclusion(:type, ~w[Document Audio Image Video])
|> validate_required([:mediaType, :url, :type])


+ 5
- 4
lib/pleroma/web/activity_pub/object_validators/audio_video_validator.ex 파일 보기

@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier

import Ecto.Changeset
@@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
embeds_many(:tag, TagValidator)
field(:type, :string)

field(:name, :string)
@@ -132,11 +132,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
data = fix(data)

struct
|> cast(data, __schema__(:fields) -- [:attachment])
|> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment)
|> cast_embed(:tag)
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Audio", "Video"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/block_validator.ex 파일 보기

@@ -26,7 +26,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Block"])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex 파일 보기

@@ -67,7 +67,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|> cast_embed(:attachment)
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["ChatMessage"])
|> validate_required([:id, :actor, :to, :type, :published])


+ 8
- 0
lib/pleroma/web/activity_pub/object_validators/common_validations.ex 파일 보기

@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object
alias Pleroma.User

@spec validate_any_presence(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_any_presence(cng, fields) do
non_empty =
fields
@@ -29,6 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end
end

@spec validate_actor_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_actor_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :actor)

@@ -47,6 +49,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end

@spec validate_object_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false)
@@ -68,6 +71,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end

@spec validate_object_or_user_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_object_or_user_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
options = Keyword.put(options, :field_name, field_name)
@@ -83,6 +87,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
if actor_cng.valid?, do: actor_cng, else: object_cng
end

@spec validate_host_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_host_match(cng, fields \\ [:id, :actor]) do
if same_domain?(cng, fields) do
cng
@@ -95,6 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end
end

@spec validate_fields_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_fields_match(cng, fields) do
if map_unique?(cng, fields) do
cng
@@ -122,12 +128,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end

@spec same_domain?(Ecto.Changeset.t(), [atom()]) :: boolean()
def same_domain?(cng, fields \\ [:actor, :object]) do
map_unique?(cng, fields, fn value -> URI.parse(value).host end)
end

# This figures out if a user is able to create, delete or modify something
# based on the domain and superuser status
@spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t()
def validate_modification_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))



+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex 파일 보기

@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
|> validate_data(meta)
end

def validate_data(cng, meta \\ []) do
defp validate_data(cng, meta) do
cng
|> validate_required([:id, :actor, :to, :type, :object])
|> validate_inclusion(:type, ["Create"])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex 파일 보기

@@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
|> CommonFixes.fix_actor()
end

def validate_data(cng, meta \\ []) do
defp validate_data(cng, meta) do
cng
|> validate_required([:actor, :type, :object])
|> validate_inclusion(:type, ["Create"])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/delete_validator.ex 파일 보기

@@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Tombstone
Video
}
def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex 파일 보기

@@ -70,7 +70,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
end
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["EmojiReact"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])


+ 5
- 4
lib/pleroma/web/activity_pub/object_validators/event_validator.ex 파일 보기

@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier

import Ecto.Changeset
@@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
embeds_many(:tag, TagValidator)
field(:type, :string)

field(:name, :string)
@@ -81,11 +81,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
data = fix(data)

struct
|> cast(data, __schema__(:fields) -- [:attachment])
|> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment)
|> cast_embed(:tag)
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Event"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/follow_validator.ex 파일 보기

@@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Follow"])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/like_validator.ex 파일 보기

@@ -76,7 +76,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
end
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])


+ 5
- 4
lib/pleroma/web/activity_pub/object_validators/question_validator.ex 파일 보기

@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier

import Ecto.Changeset
@@ -24,8 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
embeds_many(:tag, TagValidator)
field(:type, :string)
field(:content, :string)
field(:context, :string)
@@ -93,13 +93,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
data = fix(data)

struct
|> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment])
|> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment, :tag])
|> cast_embed(:attachment)
|> cast_embed(:anyOf)
|> cast_embed(:oneOf)
|> cast_embed(:tag)
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])


+ 77
- 0
lib/pleroma/web/activity_pub/object_validators/tag_validator.ex 파일 보기

@@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
use Ecto.Schema

alias Pleroma.EctoType.ActivityPub.ObjectValidators

import Ecto.Changeset

@primary_key false
embedded_schema do
# Common
field(:type, :string)
field(:name, :string)

# Mention, Hashtag
field(:href, ObjectValidators.Uri)

# Emoji
embeds_one :icon, IconObjectValidator, primary_key: false do
field(:type, :string)
field(:url, ObjectValidators.Uri)
end

field(:updated, ObjectValidators.DateTime)
field(:id, ObjectValidators.Uri)
end

def cast_and_validate(data) do
data
|> cast_data()
end

def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end

def changeset(struct, %{"type" => "Mention"} = data) do
struct
|> cast(data, [:type, :name, :href])
|> validate_required([:type, :href])
end

def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do
name =
cond do
"#" <> name -> name
name -> name
end
|> String.downcase()

data = Map.put(data, "name", name)

struct
|> cast(data, [:type, :name, :href])
|> validate_required([:type, :name])
end

def changeset(struct, %{"type" => "Emoji"} = data) do
data = Map.put(data, "name", String.trim(data["name"], ":"))

struct
|> cast(data, [:type, :name, :updated, :id])
|> cast_embed(:icon, with: &icon_changeset/2)
|> validate_required([:type, :name, :icon])
end

def icon_changeset(struct, data) do
struct
|> cast(data, [:type, :url])
|> validate_inclusion(:type, ~w[Image])
|> validate_required([:type, :url])
end
end

+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/undo_validator.ex 파일 보기

@@ -38,7 +38,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(data_cng) do
defp validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Undo"])
|> validate_required([:id, :type, :object, :actor, :to, :cc])


+ 1
- 1
lib/pleroma/web/activity_pub/object_validators/update_validator.ex 파일 보기

@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|> cast(data, __schema__(:fields))
end

def validate_data(cng) do
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Update"])


+ 12
- 13
lib/pleroma/web/activity_pub/pipeline.ex 파일 보기

@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
@spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) 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}
@@ -40,19 +41,17 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
end
end

def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, @object_validator.validate(object, meta)},
{_, {:ok, mrfd_object, meta}} <-
{:mrf_object, @mrf.pipeline_filter(validated_object, meta)},
{_, {:ok, activity, meta}} <-
{:persist_object, @activity_pub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <-
{:execute_side_effects, @side_effects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
def do_common_pipeline(%{__struct__: _}, _meta), do: {:error, :is_struct}

def do_common_pipeline(message, meta) do
with {_, {:ok, message, meta}} <- {:validate, @object_validator.validate(message, meta)},
{_, {:ok, message, meta}} <- {:mrf, @mrf.pipeline_filter(message, meta)},
{_, {:ok, message, meta}} <- {:persist, @activity_pub.persist(message, meta)},
{_, {:ok, message, meta}} <- {:side_effects, @side_effects.handle(message, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(message, meta)} do
{:ok, message, meta}
else
{:mrf_object, {:reject, message, _}} -> {:reject, message}
{:mrf, {:reject, message, _}} -> {:reject, message}
e -> {:error, e}
end
end


+ 59
- 2
lib/pleroma/web/activity_pub/side_effects.ex 파일 보기

@@ -276,10 +276,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
result =
case deleted_object do
%Object{} ->
with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
%User{} = user <- User.get_cached_by_ap_id(actor) do
User.remove_pinnned_activity(user, activity)
User.remove_pinned_object_id(user, deleted_object.data["id"])

{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)

@@ -312,6 +312,63 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end

# Tasks this handles:
# - adds pin to user
# - removes expiration job for pinned activity, if was set for expiration
@impl true
def handle(%{data: %{"type" => "Add"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.add_pinned_object_id(user, data["object"]) do
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(meta[:activity_id]) do
Oban.cancel_job(expiration.id)
end

{:ok, object, meta}
else
nil ->
{:error, :user_not_found}

{:error, changeset} ->
if changeset.errors[:pinned_objects] do
{:error, :pinned_statuses_limit_reached}
else
changeset.errors
end
end
end

# Tasks this handles:
# - removes pin from user
# - removes corresponding Add activity
# - if activity had expiration, recreates activity expiration job
@impl true
def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.remove_pinned_object_id(user, data["object"]) do
data["object"]
|> Activity.add_by_params_query(user.ap_id, user.featured_address)
|> Repo.delete_all()

# if pinned activity was scheduled for deletion, we reschedule it for deletion
if meta[:expires_at] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at])

Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: meta[:activity_id],
expires_at: expires_at
})
end

{:ok, object, meta}
else
nil -> {:error, :user_not_found}
error -> error
end
end

# Nothing to do
@impl true
def handle(object, meta) do


+ 3
- 2
lib/pleroma/web/activity_pub/transmogrifier.ex 파일 보기

@@ -534,7 +534,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def handle_incoming(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce} do
when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do
@@ -564,7 +564,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
{:error, {:validate_object, _}} = e ->
{:error, {:validate, _}} = e ->
# Check if we have a create activity for this
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <-
@@ -1000,6 +1000,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
{:ok, user} <- update_user(user, data) do
{:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
else


+ 21
- 0
lib/pleroma/web/activity_pub/views/user_view.ex 파일 보기

@@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view

alias Pleroma.Keys
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
@@ -97,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"featured" => "#{user.ap_id}/collections/featured",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
@@ -245,6 +248,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(pagination)
end

def render("featured.json", %{
user: %{featured_address: featured_address, pinned_objects: pinned_objects}
}) do
objects =
pinned_objects
|> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2)
|> Enum.map(fn {id, _} ->
ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)})
end)

%{
"id" => featured_address,
"type" => "OrderedCollection",
"orderedItems" => objects
}
|> Map.merge(Utils.make_json_ld_header())
end

defp maybe_put_total_items(map, false, _total), do: map

defp maybe_put_total_items(map, true, total) do


+ 44
- 2
lib/pleroma/web/api_spec/operations/status_operation.ex 파일 보기

@@ -182,7 +182,34 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
400 =>
Operation.response("Bad Request", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "You have already pinned the maximum number of statuses"
}
}),
404 =>
Operation.response("Not found", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Record not found"
}
}),
422 =>
Operation.response(
"Unprocessable Entity",
"application/json",
%Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Someone else's status cannot be pinned"
}
}
)
}
}
end
@@ -197,7 +224,22 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
400 =>
Operation.response("Bad Request", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "You have already pinned the maximum number of statuses"
}
}),
404 =>
Operation.response("Not found", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Record not found"
}
})
}
}
end


+ 7
- 0
lib/pleroma/web/api_spec/schemas/status.ex 파일 보기

@@ -194,6 +194,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
parent_visible: %Schema{
type: :boolean,
description: "`true` if the parent post is visible to the user"
},
pinned_at: %Schema{
type: :string,
format: "date-time",
nullable: true,
description:
"A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned"
}
}
},


+ 46
- 27
lib/pleroma/web/common_api.ex 파일 보기

@@ -228,17 +228,7 @@ defmodule Pleroma.Web.CommonAPI do
{:find_object, _} ->
{:error, :not_found}

{:common_pipeline,
{
:error,
{
:validate_object,
{
:error,
changeset
}
}
}} = e ->
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
else
@@ -411,29 +401,58 @@ defmodule Pleroma.Web.CommonAPI do
end
end

def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}}
} = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id),
true <- object_type_is_allowed_for_pin(activity.object),
true <- activity_is_public(activity),
{:ok, pin_data, _} <- Builder.pin(user, activity.object),
{:ok, _pin, _} <-
Pipeline.common_pipeline(pin_data,
local: true,
activity_id: id
) do
{:ok, activity}
else
{:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not pin")}
{:error, {:side_effects, error}} -> error
error -> error
end
end

defp create_activity_by_id(id) do
with nil <- Activity.create_by_id_with_object(id) do
{:error, :not_found}
end
end

defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}

defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed}
end
end

defp activity_is_public(activity) do
with false <- Visibility.is_public?(activity) do
{:error, :visibility_error}
end
end

@spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
def unpin(id, user) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
{:ok, _unpin, _} <-
Pipeline.common_pipeline(unpin_data,
local: true,
activity_id: activity.id,
expires_at: activity.data["expires_at"],
featured_address: user.featured_address
) do
{:ok, activity}
else
{:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not unpin")}
end
end



+ 6
- 0
lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex 파일 보기

@@ -30,6 +30,12 @@ defmodule Pleroma.Web.MastodonAPI.FallbackController do
|> json(%{error: error_message})
end

def call(conn, {:error, status, message}) do
conn
|> put_status(status)
|> json(%{error: message})
end

def call(conn, _) do
conn
|> put_status(:internal_server_error)


+ 12
- 0
lib/pleroma/web/mastodon_api/controllers/status_controller.ex 파일 보기

@@ -260,6 +260,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
{:error, :pinned_statuses_limit_reached} ->
{:error, "You have already pinned the maximum number of statuses"}

{:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be pinned"}

{:error, :visibility_error} ->
{:error, :unprocessable_entity, "Non-public status cannot be pinned"}

error ->
error
end
end



+ 2
- 1
lib/pleroma/web/mastodon_api/views/instance_view.ex 파일 보기

@@ -23,7 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
streaming_api: Pleroma.Web.Endpoint.websocket_url()
},
stats: Pleroma.Stats.get_stats(),
thumbnail: Pleroma.Web.base_url() <> Keyword.get(instance, :instance_thumbnail),
thumbnail:
URI.merge(Pleroma.Web.base_url(), Keyword.get(instance, :instance_thumbnail)) |> to_string,
languages: ["en"],
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),


+ 17
- 6
lib/pleroma/web/mastodon_api/views/status_view.ex 파일 보기

@@ -152,6 +152,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

{pinned?, pinned_at} = pin_data(object, user)

%{
id: to_string(activity.id),
uri: object.data["id"],
@@ -173,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
pinned: pinned?,
sensitive: false,
spoiler_text: "",
visibility: get_visibility(activity),
@@ -184,7 +186,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
language: nil,
emojis: [],
pleroma: %{
local: activity.local
local: activity.local,
pinned_at: pinned_at
}
}
end
@@ -316,6 +319,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
fn for_user, user -> User.mutes?(for_user, user) end
)

{pinned?, pinned_at} = pin_data(object, user)

%{
id: to_string(activity.id),
uri: object.data["id"],
@@ -339,7 +344,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: muted,
pinned: pinned?(activity, user),
pinned: pinned?,
sensitive: sensitive,
spoiler_text: summary,
visibility: get_visibility(object),
@@ -360,7 +365,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for])
parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at
}
}
end
@@ -529,8 +535,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp present?(false), do: false
defp present?(_), do: true

defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
if pinned_at = pinned_objects[object_id] do
{true, Utils.to_masto_date(pinned_at)}
else
{false, nil}
end
end

defp build_emoji_map(emoji, users, current_user) do
%{


+ 2
- 1
lib/pleroma/web/plugs/http_security_plug.ex 파일 보기

@@ -48,7 +48,8 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
{"x-content-type-options", "nosniff"},
{"referrer-policy", referrer_policy},
{"x-download-options", "noopen"},
{"content-security-policy", csp_string()}
{"content-security-policy", csp_string()},
{"permissions-policy", "interest-cohort=()"}
]

headers =


+ 1
- 0
lib/pleroma/web/router.ex 파일 보기

@@ -704,6 +704,7 @@ defmodule Pleroma.Web.Router do
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
end

scope "/", Pleroma.Web.ActivityPub do


+ 1
- 1
mix.exs 파일 보기

@@ -38,7 +38,7 @@ defmodule Pleroma.Mixfile do
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load, eldap: :transient],
steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1],
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, nil}]
config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}]
]
]
]


+ 9
- 0
priv/repo/migrations/20210202110641_add_pinned_objects_to_users.exs 파일 보기

@@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddPinnedObjectsToUsers do
use Ecto.Migration

def change do
alter table(:users) do
add(:pinned_objects, :map)
end
end
end

+ 23
- 0
priv/repo/migrations/20210203141144_add_featured_address_to_users.exs 파일 보기

@@ -0,0 +1,23 @@
defmodule Pleroma.Repo.Migrations.AddFeaturedAddressToUsers do
use Ecto.Migration

def up do
alter table(:users) do
add(:featured_address, :string)
end

create(index(:users, [:featured_address]))

execute("""

update users set featured_address = concat(ap_id, '/collections/featured') where local = true and featured_address is null;

""")
end

def down do
alter table(:users) do
remove(:featured_address)
end
end
end

+ 28
- 0
priv/repo/migrations/20210205145000_move_pinned_activities_into_pinned_objects.exs 파일 보기

@@ -0,0 +1,28 @@
defmodule Pleroma.Repo.Migrations.MovePinnedActivitiesIntoPinnedObjects do
use Ecto.Migration

import Ecto.Query

alias Pleroma.Repo
alias Pleroma.User

def up do
from(u in User)
|> select([u], {u.id, fragment("?.pinned_activities", u)})
|> Repo.stream()
|> Stream.each(fn {user_id, pinned_activities_ids} ->
pinned_activities = Pleroma.Activity.all_by_ids_with_object(pinned_activities_ids)

pins =
Map.new(pinned_activities, fn %{object: %{data: %{"id" => object_id}}} ->
{object_id, NaiveDateTime.utc_now()}
end)

from(u in User, where: u.id == ^user_id)
|> Repo.update_all(set: [pinned_objects: pins])
end)
|> Stream.run()
end

def down, do: :noop
end

+ 15
- 0
priv/repo/migrations/20210206045221_remove_pinned_activities_from_users.exs 파일 보기

@@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.RemovePinnedActivitiesFromUsers do
use Ecto.Migration

def up do
alter table(:users) do
remove(:pinned_activities)
end
end

def down do
alter table(:users) do
add(:pinned_activities, {:array, :string}, default: [])
end
end
end

+ 17
- 0
priv/repo/migrations/20210401143153_user_notification_settings_fix.exs 파일 보기

@@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.UserNotificationSettingsFix do
use Ecto.Migration

def up do
execute(~s(UPDATE users
SET
notification_settings = '{"followers": true, "follows": true, "non_follows": true, "non_followers": true}'::jsonb WHERE notification_settings IS NULL
))

execute("ALTER TABLE users
ALTER COLUMN notification_settings SET NOT NULL")
end

def down do
:ok
end
end

+ 5
- 0
test/fixtures/config/temp.exported_from_db.secret.exs 파일 보기

@@ -0,0 +1,5 @@
use Mix.Config

config :pleroma, exported_config_merged: true

config :pleroma, :first_setting, key: "new value"

+ 39
- 0
test/fixtures/mastodon/collections/featured.json 파일 보기

@@ -0,0 +1,39 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://{{domain}}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"id": "https://{{domain}}/users/{{nickname}}/collections/featured",
"orderedItems": [
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://{{domain}}/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"actor": "https://{{domain}}/users/{{nickname}}",
"attachment": [],
"attributedTo": "https://{{domain}}/users/{{nickname}}",
"cc": [
"https://{{domain}}/users/{{nickname}}/followers"
],
"content": "",
"id": "https://{{domain}}/objects/{{object_id}}",
"published": "2021-02-12T15:13:43.915429Z",
"sensitive": false,
"source": "",
"summary": "",
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
}
],
"type": "OrderedCollection"
}

+ 47
- 0
test/fixtures/statuses/masto-note.json 파일 보기

@@ -0,0 +1,47 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount"
}
],
"id": "https://example.com/users/{{nickname}}/statuses/{{status_id}}",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2021-02-24T12:40:49Z",
"url": "https://example.com/@{{nickname}}/{{status_id}}",
"attributedTo": "https://example.com/users/{{nickname}}",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.com/users/{{nickname}}/followers"
],
"sensitive": false,
"atomUri": "https://example.com/users/{{nickname}}/statuses/{{status_id}}",
"inReplyToAtomUri": null,
"conversation": "tag:example.com,2021-02-24:objectId=15:objectType=Conversation",
"content": "<p></p>",
"contentMap": {
"en": "<p></p>"
},
"attachment": [],
"tag": [],
"replies": {
"id": "https://example.com/users/{{nickname}}/statuses/{{status_id}}/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.com/users/{{nickname}}/statuses/{{status_id}}/replies?only_other_accounts=true&page=true",
"partOf": "https://example.com/users/{{nickname}}/statuses/{{status_id}}/replies",
"items": []
}
}
}

+ 27
- 0
test/fixtures/statuses/note.json 파일 보기

@@ -0,0 +1,27 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://example.com/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"actor": "https://example.com/users/{{nickname}}",
"attachment": [],
"attributedTo": "https://example.com/users/{{nickname}}",
"cc": [
"https://example.com/users/{{nickname}}/followers"
],
"content": "Content",
"context": "https://example.com/contexts/e4b180e1-7403-477f-aeb4-de57e7a3fe7f",
"conversation": "https://example.com/contexts/e4b180e1-7403-477f-aeb4-de57e7a3fe7f",
"id": "https://example.com/objects/{{object_id}}",
"published": "2019-12-15T22:00:05.279583Z",
"sensitive": false,
"summary": "",
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
}

+ 18
- 0
test/fixtures/users_mock/masto_featured.json 파일 보기

@@ -0,0 +1,18 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount"
}
],
"id": "https://{{domain}}/users/{{nickname}}/collections/featured",
"type": "OrderedCollection",
"totalItems": 0,
"orderedItems": []
}

+ 42
- 0
test/fixtures/users_mock/user.json 파일 보기

@@ -0,0 +1,42 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://example.com/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"attachment": [],
"endpoints": {
"oauthAuthorizationEndpoint": "https://example.com/oauth/authorize",
"oauthRegistrationEndpoint": "https://example.com/api/v1/apps",
"oauthTokenEndpoint": "https://example.com/oauth/token",
"sharedInbox": "https://example.com/inbox"
},
"followers": "https://example.com/users/{{nickname}}/followers",
"following": "https://example.com/users/{{nickname}}/following",
"icon": {
"type": "Image",
"url": "https://example.com/media/4e914f5b84e4a259a3f6c2d2edc9ab642f2ab05f3e3d9c52c81fc2d984b3d51e.jpg"
},
"id": "https://example.com/users/{{nickname}}",
"image": {
"type": "Image",
"url": "https://example.com/media/f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg?name=f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg"
},
"inbox": "https://example.com/users/{{nickname}}/inbox",
"manuallyApprovesFollowers": false,
"name": "{{nickname}}",
"outbox": "https://example.com/users/{{nickname}}/outbox",
"preferredUsername": "{{nickname}}",
"publicKey": {
"id": "https://example.com/users/{{nickname}}#main-key",
"owner": "https://example.com/users/{{nickname}}",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5DLtwGXNZElJyxFGfcVc\nXANhaMadj/iYYQwZjOJTV9QsbtiNBeIK54PJrYuU0/0YIdrvS1iqheX5IwXRhcwa\nhm3ZyLz7XeN9st7FBni4BmZMBtMpxAuYuu5p/jbWy13qAiYOhPreCx0wrWgm/lBD\n9mkgaxIxPooBE0S4ZWEJIDIV1Vft3AWcRUyWW1vIBK0uZzs6GYshbQZB952S0yo4\nFzI1hABGHncH8UvuFauh4EZ8tY7/X5I0pGRnDOcRN1dAht5w5yTA+6r5kebiFQjP\nIzN/eCO/a9Flrj9YGW7HDNtjSOH0A31PLRGlJtJO3yK57dnf5ppyCZGfL4emShQo\ncQIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"featured": "https://example.com/users/{{nickname}}/collections/featured",
"summary": "your friendly neighborhood pleroma developer<br>I like cute things and distributed systems, and really hate delete and redrafts",
"tag": [],
"type": "Person",
"url": "https://example.com/users/{{nickname}}"
}

+ 22
- 0
test/pleroma/activity_test.exs 파일 보기

@@ -254,4 +254,26 @@ defmodule Pleroma.ActivityTest do

assert %{id: ^id} = Activity.get_by_object_ap_id_with_object(obj_id)
end

test "add_by_params_query/3" do
user = insert(:user)

note = insert(:note_activity, user: user)

insert(:add_activity, user: user, note: note)
insert(:add_activity, user: user, note: note)
insert(:add_activity, user: user)

assert Repo.aggregate(Activity, :count, :id) == 4

add_query =
Activity.add_by_params_query(note.data["object"], user.ap_id, user.featured_address)

assert Repo.aggregate(add_query, :count, :id) == 2

Repo.delete_all(add_query)
assert Repo.aggregate(add_query, :count, :id) == 0

assert Repo.aggregate(Activity, :count, :id) == 2
end
end

+ 45
- 0
test/pleroma/config/release_runtime_provider_test.exs 파일 보기

@@ -0,0 +1,45 @@
defmodule Pleroma.Config.ReleaseRuntimeProviderTest do
use ExUnit.Case, async: true

alias Pleroma.Config.ReleaseRuntimeProvider

describe "load/2" do
test "loads release defaults config and warns about non-existent runtime config" do
ExUnit.CaptureIO.capture_io(fn ->
merged = ReleaseRuntimeProvider.load([], [])
assert merged == Pleroma.Config.Holder.release_defaults()
end) =~
"!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file"
end

test "merged runtime config" do
merged =
ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs")

assert merged[:pleroma][:first_setting] == [key: "value", key2: [Pleroma.Repo]]
assert merged[:pleroma][:second_setting] == [key: "value2", key2: ["Activity"]]
end

test "merged exported config" do
ExUnit.CaptureIO.capture_io(fn ->
merged =
ReleaseRuntimeProvider.load([],
exported_config_path: "test/fixtures/config/temp.exported_from_db.secret.exs"
)

assert merged[:pleroma][:exported_config_merged]
end) =~
"!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file"
end

test "runtime config is merged with exported config" do
merged =
ReleaseRuntimeProvider.load([],
config_path: "test/fixtures/config/temp.secret.exs",
exported_config_path: "test/fixtures/config/temp.exported_from_db.secret.exs"
)

assert merged[:pleroma][:first_setting] == [key2: [Pleroma.Repo], key: "new value"]
end
end
end

+ 45
- 0
test/pleroma/user_test.exs 파일 보기

@@ -2338,4 +2338,49 @@ defmodule Pleroma.UserTest do
assert User.active_user_count(6) == 3
assert User.active_user_count(1) == 1
end

describe "pins" do
setup do
user = insert(:user)

[user: user, object_id: object_id_from_created_activity(user)]
end

test "unique pins", %{user: user, object_id: object_id} do
assert {:ok, %{pinned_objects: %{^object_id => pinned_at1} = pins} = updated_user} =
User.add_pinned_object_id(user, object_id)

assert Enum.count(pins) == 1

assert {:ok, %{pinned_objects: %{^object_id => pinned_at2} = pins}} =
User.add_pinned_object_id(updated_user, object_id)

assert pinned_at1 == pinned_at2

assert Enum.count(pins) == 1
end

test "respects max_pinned_statuses limit", %{user: user, object_id: object_id} do
clear_config([:instance, :max_pinned_statuses], 1)
{:ok, updated} = User.add_pinned_object_id(user, object_id)

object_id2 = object_id_from_created_activity(user)

{:error, %{errors: errors}} = User.add_pinned_object_id(updated, object_id2)
assert Keyword.has_key?(errors, :pinned_objects)
end

test "remove_pinned_object_id/2", %{user: user, object_id: object_id} do
assert {:ok, updated} = User.add_pinned_object_id(user, object_id)

{:ok, after_remove} = User.remove_pinned_object_id(updated, object_id)
assert after_remove.pinned_objects == %{}
end
end

defp object_id_from_created_activity(user) do
%{id: id} = insert(:note_activity, user: user)
%{object: %{data: %{"id" => object_id}}} = Activity.get_by_id_with_object(id)
object_id
end
end

+ 205
- 0
test/pleroma/web/activity_pub/activity_pub_controller_test.exs 파일 보기

@@ -636,6 +636,186 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> post("/inbox", non_create_data)
|> json_response(400)
end

test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"

status =
File.read!("test/fixtures/statuses/note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)

object_url = "https://example.com/objects/#{object_id}"

user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")

actor = "https://example.com/users/lain"

Tesla.Mock.mock(fn
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}

%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}

%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)

data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add",
"to" => [Pleroma.Constants.as_public()]
}

assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)

ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]

data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()]
}

assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)

ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end

test "mastodon pin/unpin", %{conn: conn} do
status_id = "105786274556060421"

status =
File.read!("test/fixtures/statuses/masto-note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{status_id}}", status_id)

status_url = "https://example.com/users/lain/statuses/#{status_id}"

user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")

actor = "https://example.com/users/lain"

Tesla.Mock.mock(fn
%{
method: :get,
url: ^status_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}

%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}

%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)

data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add"
}

assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)

ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]

data = %{
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove"
}

assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)

ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end
end

describe "/users/:nickname/inbox" do
@@ -1772,4 +1952,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> json_response(403)
end
end

test "pinned collection", %{conn: conn} do
clear_config([:instance, :max_pinned_statuses], 2)
user = insert(:user)
objects = insert_list(2, :note, user: user)

Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user ->
{:ok, updated} = User.add_pinned_object_id(user, object_id)
updated
end)

%{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} =
refresh_record(user)

%{"id" => ^featured_address, "orderedItems" => items} =
conn
|> get("/users/#{nickname}/collections/featured")
|> json_response(200)

object_ids = Enum.map(items, & &1["id"])

assert Enum.all?(pinned_objects, fn {obj_id, _} ->
obj_id in object_ids
end)
end
end

+ 77
- 0
test/pleroma/web/activity_pub/activity_pub_test.exs 파일 보기

@@ -235,6 +235,83 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}]
}
end

test "fetches user featured collection" do
ap_id = "https://example.com/users/lain"

featured_url = "https://example.com/users/lain/collections/featured"

user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> Jason.decode!()
|> Map.put("featured", featured_url)
|> Jason.encode!()

object_id = Ecto.UUID.generate()

featured_data =
"test/fixtures/mastodon/collections/featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)

object_url = "https://example.com/objects/#{object_id}"

object_data =
"test/fixtures/statuses/note.json"
|> File.read!()
|> String.replace("{{object_id}}", object_id)
|> String.replace("{{nickname}}", "lain")

Tesla.Mock.mock(fn
%{
method: :get,
url: ^ap_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}

%{
method: :get,
url: ^featured_url
} ->
%Tesla.Env{
status: 200,
body: featured_data,
headers: [{"content-type", "application/activity+json"}]
}
end)

Tesla.Mock.mock_global(fn
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: object_data,
headers: [{"content-type", "application/activity+json"}]
}
end)

{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
Process.sleep(50)

assert user.featured_address == featured_url
assert Map.has_key?(user.pinned_objects, object_url)

in_db = Pleroma.User.get_by_ap_id(ap_id)
assert in_db.featured_address == featured_url
assert Map.has_key?(user.pinned_objects, object_url)

assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
end
end

test "it fetches the appropriate tag-restricted posts" do


+ 126
- 0
test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs 파일 보기

@@ -0,0 +1,126 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicyTest do
use Pleroma.DataCase, async: true

alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF.FollowBotPolicy

import Pleroma.Factory

describe "FollowBotPolicy" do
test "follows remote users" do
bot = insert(:user, actor_type: "Service")
remote_user = insert(:user, local: false)
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)

message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => [remote_user.follower_address],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create",
"object" => %{
"content" => "Test post",
"type" => "Note",
"attributedTo" => remote_user.ap_id,
"inReplyTo" => nil
},
"actor" => remote_user.ap_id
}

refute User.following?(bot, remote_user)

assert User.get_follow_requests(remote_user) |> length == 0

FollowBotPolicy.filter(message)

assert User.get_follow_requests(remote_user) |> length == 1
end

test "does not follow users with #nobot in bio" do
bot = insert(:user, actor_type: "Service")
remote_user = insert(:user, %{local: false, bio: "go away bots! #nobot"})
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)

message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => [remote_user.follower_address],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create",
"object" => %{
"content" => "I don't like follow bots",
"type" => "Note",
"attributedTo" => remote_user.ap_id,
"inReplyTo" => nil
},
"actor" => remote_user.ap_id
}

refute User.following?(bot, remote_user)

assert User.get_follow_requests(remote_user) |> length == 0

FollowBotPolicy.filter(message)

assert User.get_follow_requests(remote_user) |> length == 0
end

test "does not follow local users" do
bot = insert(:user, actor_type: "Service")
local_user = insert(:user, local: true)
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)

message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => [local_user.follower_address],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create",
"object" => %{
"content" => "Hi I'm a local user",
"type" => "Note",
"attributedTo" => local_user.ap_id,
"inReplyTo" => nil
},
"actor" => local_user.ap_id
}

refute User.following?(bot, local_user)

assert User.get_follow_requests(local_user) |> length == 0

FollowBotPolicy.filter(message)

assert User.get_follow_requests(local_user) |> length == 0
end

test "does not follow users requiring follower approval" do
bot = insert(:user, actor_type: "Service")
remote_user = insert(:user, %{local: false, is_locked: true})
clear_config([:mrf_follow_bot, :follower_nickname], bot.nickname)

message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => [remote_user.follower_address],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create",
"object" => %{
"content" => "I don't like randos following me",
"type" => "Note",
"attributedTo" => remote_user.ap_id,
"inReplyTo" => nil
},
"actor" => remote_user.ap_id
}

refute User.following?(bot, remote_user)

assert User.get_follow_requests(remote_user) |> length == 0

FollowBotPolicy.filter(message)

assert User.get_follow_requests(remote_user) |> length == 0
end
end
end

+ 16
- 7
test/pleroma/web/activity_pub/pipeline_test.exs 파일 보기

@@ -25,9 +25,6 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
MRFMock
|> expect(:pipeline_filter, fn o, m -> {:ok, o, m} end)

ActivityPubMock
|> expect(:persist, fn o, m -> {:ok, o, m} end)

SideEffectsMock
|> expect(:handle, fn o, m -> {:ok, o, m} end)
|> expect(:handle_after_transaction, fn m -> m end)
@@ -42,6 +39,9 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do

activity_with_object = %{activity | data: Map.put(activity.data, "object", object)}

ActivityPubMock
|> expect(:persist, fn _, m -> {:ok, activity, m} end)

FederatorMock
|> expect(:publish, fn ^activity_with_object -> :ok end)

@@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do

assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(
activity,
activity.data,
meta
)
end
@@ -59,6 +59,9 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
activity = insert(:note_activity)
meta = [local: true]

ActivityPubMock
|> expect(:persist, fn _, m -> {:ok, activity, m} end)

FederatorMock
|> expect(:publish, fn ^activity -> :ok end)

@@ -66,29 +69,35 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
|> expect(:get, fn [:instance, :federating] -> true end)

assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity.data, meta)
end

test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do
activity = insert(:note_activity)
meta = [local: false]

ActivityPubMock
|> expect(:persist, fn _, m -> {:ok, activity, m} end)

ConfigMock
|> expect(:get, fn [:instance, :federating] -> true end)

assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity.data, meta)
end

test "it goes through validation, filtering, persisting, side effects without federation for local activities if federation is deactivated" do
activity = insert(:note_activity)
meta = [local: true]

ActivityPubMock
|> expect(:persist, fn _, m -> {:ok, activity, m} end)

ConfigMock
|> expect(:get, fn [:instance, :federating] -> false end)

assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity.data, meta)
end
end
end

+ 172
- 0
test/pleroma/web/activity_pub/transmogrifier/add_remove_handling_test.exs 파일 보기

@@ -0,0 +1,172 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier.AddRemoveHandlingTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase, async: true

require Pleroma.Constants

import Pleroma.Factory

alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier

test "it accepts Add/Remove activities" do
user =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")

object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"

object =
"test/fixtures/statuses/note.json"
|> File.read!()
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)

object_url = "https://example.com/objects/#{object_id}"

actor = "https://example.com/users/lain"

Tesla.Mock.mock(fn
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}

%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: object,
headers: [{"content-type", "application/activity+json"}]
}

%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)

message = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add",
"to" => [Pleroma.Constants.as_public()],
"cc" => ["https://example.com/users/lain/followers"]
}

assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data == message
user = User.get_cached_by_ap_id(actor)
assert user.pinned_objects[object_url]

remove = %{
"id" => "http://localhost:400/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()],
"cc" => ["https://example.com/users/lain/followers"]
}

assert {:ok, activity} = Transmogrifier.handle_incoming(remove)
assert activity.data == remove

user = refresh_record(user)
refute user.pinned_objects[object_url]
end

test "Add/Remove activities for remote users without featured address" do
user = insert(:user, local: false, domain: "example.com")

user =
user
|> Ecto.Changeset.change(featured_address: nil)
|> Repo.update!()

%{host: host} = URI.parse(user.ap_id)

user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", user.nickname)

object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"

object =
"test/fixtures/statuses/note.json"
|> File.read!()
|> String.replace("{{nickname}}", user.nickname)
|> String.replace("{{object_id}}", object_id)

object_url = "https://#{host}/objects/#{object_id}"

actor = "https://#{host}/users/#{user.nickname}"

featured = "https://#{host}/users/#{user.nickname}/collections/featured"

Tesla.Mock.mock(fn
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}

%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: object,
headers: [{"content-type", "application/activity+json"}]
}

%{method: :get, url: ^featured} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "#{host}")
|> String.replace("{{nickname}}", user.nickname),
headers: [{"content-type", "application/activity+json"}]
}
end)

message = %{
"id" => "https://#{host}/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
"actor" => actor,
"object" => object_url,
"target" => "https://#{host}/users/#{user.nickname}/collections/featured",
"type" => "Add",
"to" => [Pleroma.Constants.as_public()],
"cc" => ["https://#{host}/users/#{user.nickname}/followers"]
}

assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data == message
user = User.get_cached_by_ap_id(actor)
assert user.pinned_objects[object_url]
end
end

+ 76
- 0
test/pleroma/web/admin_api/controllers/config_controller_test.exs 파일 보기

@@ -1410,6 +1410,82 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
end

test "custom instance thumbnail", %{conn: conn} do
clear_config([:instance])

params = %{
"group" => ":pleroma",
"key" => ":instance",
"value" => [
%{
"tuple" => [
":instance_thumbnail",
"https://example.com/media/new_thumbnail.jpg"
]
}
]
}

res =
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{"configs" => [params]})
|> json_response_and_validate_schema(200)

assert res == %{
"configs" => [
%{
"db" => [":instance_thumbnail"],
"group" => ":pleroma",
"key" => ":instance",
"value" => params["value"]
}
],
"need_reboot" => false
}

_res =
assert conn
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)

assert res = %{"thumbnail" => "https://example.com/media/new_thumbnail.jpg"}
end

test "Concurrent Limiter", %{conn: conn} do
clear_config([ConcurrentLimiter])

params = %{
"group" => ":pleroma",
"key" => "ConcurrentLimiter",
"value" => [
%{
"tuple" => [
"Pleroma.Web.RichMedia.Helpers",
[
%{"tuple" => [":max_running", 6]},
%{"tuple" => [":max_waiting", 6]}
]
]
},
%{
"tuple" => [
"Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy",
[
%{"tuple" => [":max_running", 7]},
%{"tuple" => [":max_waiting", 7]}
]
]
}
]
}

assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{"configs" => [params]})
|> json_response_and_validate_schema(200)
end
end

describe "GET /api/pleroma/admin/config/descriptions" do


+ 51
- 9
test/pleroma/web/common_api_test.exs 파일 보기

@@ -827,13 +827,17 @@ defmodule Pleroma.Web.CommonAPITest do
[user: user, activity: activity]
end

test "activity not found error", %{user: user} do
assert {:error, :not_found} = CommonAPI.pin("id", user)
end

test "pin status", %{user: user, activity: activity} do
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)

id = activity.id
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)

assert %User{pinned_activities: [^id]} = user
assert user.pinned_objects |> Map.keys() == [object_id]
end

test "pin poll", %{user: user} do
@@ -845,10 +849,11 @@ defmodule Pleroma.Web.CommonAPITest do

assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)

id = activity.id
%{data: %{"id" => object_id}} = Object.normalize(activity)

user = refresh_record(user)

assert %User{pinned_activities: [^id]} = user
assert user.pinned_objects |> Map.keys() == [object_id]
end

test "unlisted statuses can be pinned", %{user: user} do
@@ -859,7 +864,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "only self-authored can be pinned", %{activity: activity} do
user = insert(:user)

assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user)
assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user)
end

test "max pinned statuses", %{user: user, activity: activity_one} do
@@ -869,8 +874,12 @@ defmodule Pleroma.Web.CommonAPITest do

user = refresh_record(user)

assert {:error, "You have already pinned the maximum number of statuses"} =
CommonAPI.pin(activity_two.id, user)
assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user)
end

test "only public can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
{:error, :visibility_error} = CommonAPI.pin(activity.id, user)
end

test "unpin status", %{user: user, activity: activity} do
@@ -884,7 +893,7 @@ defmodule Pleroma.Web.CommonAPITest do

user = refresh_record(user)

assert %User{pinned_activities: []} = user
assert user.pinned_objects == %{}
end

test "should unpin when deleting a status", %{user: user, activity: activity} do
@@ -896,7 +905,40 @@ defmodule Pleroma.Web.CommonAPITest do

user = refresh_record(user)

assert %User{pinned_activities: []} = user
assert user.pinned_objects == %{}
end

test "ephemeral activity won't be deleted if was pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601})

assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)

{:ok, _activity} = CommonAPI.pin(activity.id, user)
refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)

user = refresh_record(user)
{:ok, _} = CommonAPI.unpin(activity.id, user)

# recreates expiration job on unpin
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
end

test "ephemeral activity deletion job won't be deleted on pinning error", %{
user: user,
activity: activity
} do
clear_config([:instance, :max_pinned_statuses], 1)

{:ok, _activity} = CommonAPI.pin(activity.id, user)

{:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601})

assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)

user = refresh_record(user)
{:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user)

assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
end
end



+ 27
- 11
test/pleroma/web/mastodon_api/controllers/status_controller_test.exs 파일 보기

@@ -1209,20 +1209,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
setup do: clear_config([:instance, :max_pinned_statuses], 1)

test "pin status", %{conn: conn, user: user, activity: activity} do
id_str = to_string(activity.id)
id = activity.id

assert %{"id" => ^id_str, "pinned" => true} =
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(200)

assert [%{"id" => ^id_str, "pinned" => true}] =
assert [%{"id" => ^id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end

test "non authenticated user", %{activity: activity} do
assert build_conn()
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(403) == %{"error" => "Invalid credentials."}
end

test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})

@@ -1231,7 +1238,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{dm.id}/pin")

assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"}
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "Non-public status cannot be pinned"
}
end

test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"])

assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
end

test "unpin status", %{conn: conn, user: user, activity: activity} do
@@ -1252,13 +1270,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(200)
end

test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")

assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"}
test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end

test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do


+ 2
- 1
test/pleroma/web/mastodon_api/views/status_view_test.exs 파일 보기

@@ -286,7 +286,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
direct_conversation_id: nil,
thread_muted: false,
emoji_reactions: [],
parent_visible: false
parent_visible: false,
pinned_at: nil
}
}



+ 30
- 0
test/pleroma/web/twitter_api/remote_follow_controller_test.exs 파일 보기

@@ -27,6 +27,16 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
body: File.read!("test/fixtures/tesla_mock/status.emelie.json")
}

%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}

%{method: :get, url: "https://mastodon.social/users/emelie"} ->
%Tesla.Env{
status: 200,
@@ -52,6 +62,16 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}

%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
end)

response =
@@ -70,6 +90,16 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}

%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
end)

user = insert(:user)


+ 49
- 3
test/support/factory.ex 파일 보기

@@ -4,6 +4,9 @@

defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo

require Pleroma.Constants

alias Pleroma.Object
alias Pleroma.User

@@ -41,23 +44,27 @@ defmodule Pleroma.Factory do

urls =
if attrs[:local] == false do
base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])
base_domain = attrs[:domain] || Enum.random(["domain1.com", "domain2.com", "domain3.com"])

ap_id = "https://#{base_domain}/users/#{user.nickname}"

%{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured"
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
following_address: User.ap_following(user),
featured_address: User.ap_featured_collection(user)
}
end

attrs = Map.delete(attrs, :domain)

user
|> Map.put(:raw_bio, user.bio)
|> Map.merge(urls)
@@ -221,6 +228,45 @@ defmodule Pleroma.Factory do
}
end

def add_activity_factory(attrs \\ %{}) do
featured_collection_activity(attrs, "Add")
end

def remove_activity_factor(attrs \\ %{}) do
featured_collection_activity(attrs, "Remove")
end

defp featured_collection_activity(attrs, type) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)

data_attrs =
attrs
|> Map.get(:data_attrs, %{})
|> Map.put(:type, type)

attrs = Map.drop(attrs, [:user, :note, :data_attrs])

data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"target" => user.featured_address,
"object" => note.data["object"],
"actor" => note.data["actor"],
"type" => "Add",
"to" => [Pleroma.Constants.as_public()],
"cc" => [user.follower_address]
}
|> Map.merge(data_attrs)

%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end

def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)


+ 24
- 0
test/support/http_request_mock.ex 파일 보기

@@ -89,6 +89,18 @@ defmodule HttpRequestMock do
}}
end

def get("https://mastodon.sdf.org/users/rinpatch/collections/featured", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.sdf.org")
|> String.replace("{{nickname}}", "rinpatch"),
headers: [{"content-type", "application/activity+json"}]
}}
end

def get("https://patch.cx/objects/tesla_mock/poll_attachment", _, _, _) do
{:ok,
%Tesla.Env{
@@ -905,6 +917,18 @@ defmodule HttpRequestMock do
}}
end

def get("https://mastodon.social/users/lambadalambda/collections/featured", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "lambadalambda"),
headers: activitypub_object_headers()
}}
end

def get("https://apfed.club/channel/indio", _, _, _) do
{:ok,
%Tesla.Env{


불러오는 중...
취소
저장