diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8f1839c42..dab52e4c6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,10 +26,10 @@ cache: &global_cache_policy - _build stages: - - check-changelog - build - lint - test + - check-changelog - benchmark - deploy - release @@ -113,7 +113,7 @@ benchmark: variables: MIX_ENV: benchmark services: - - name: postgres:9.6-alpine + - name: postgres:11.22-alpine alias: postgres command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] script: @@ -169,25 +169,6 @@ unit-testing-1.12-erratic: - mix ecto.migrate - mix test --only=erratic -unit-testing-1.12-rum: - extends: - - .build_changes_policy - - .using-ci-base - stage: test - cache: *testing_cache_policy - services: - - name: git.pleroma.social:5050/pleroma/pleroma/postgres-with-rum-13 - alias: postgres - command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - variables: - <<: *global_variables - RUM_ENABLED: "true" - script: - - mix ecto.create - - mix ecto.migrate - - "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/" - - mix test --preload-modules - formatting-1.13: extends: .build_changes_policy image: &formatting_elixir elixir:1.13-alpine diff --git a/changelog.d/backups-follows.add b/changelog.d/backups-follows.add new file mode 100644 index 000000000..a55c436f6 --- /dev/null +++ b/changelog.d/backups-follows.add @@ -0,0 +1 @@ +Include following/followers in backups \ No newline at end of file diff --git a/changelog.d/bookmark-folders.add b/changelog.d/bookmark-folders.add new file mode 100644 index 000000000..d9b03cecc --- /dev/null +++ b/changelog.d/bookmark-folders.add @@ -0,0 +1 @@ +Allow to group bookmarks in folders \ No newline at end of file diff --git a/changelog.d/bookmark-folders.skip b/changelog.d/bookmark-folders.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/card-image-description.add b/changelog.d/card-image-description.add new file mode 100644 index 000000000..bf423ebb8 --- /dev/null +++ b/changelog.d/card-image-description.add @@ -0,0 +1 @@ +Include image description in status media cards \ No newline at end of file diff --git a/changelog.d/fep-2c59.add b/changelog.d/fep-2c59.add new file mode 100644 index 000000000..03e33cbd8 --- /dev/null +++ b/changelog.d/fep-2c59.add @@ -0,0 +1 @@ +Implement FEP-2c59, add "webfinger" to user actor \ No newline at end of file diff --git a/changelog.d/ffmpeg-limiter.add b/changelog.d/ffmpeg-limiter.add new file mode 100644 index 000000000..e4a5ef196 --- /dev/null +++ b/changelog.d/ffmpeg-limiter.add @@ -0,0 +1 @@ +Framegrabs with ffmpeg will execute with a 5 second timeout and cache the URLs of failures with a TTL of 15 minutes to prevent excessive retries. diff --git a/changelog.d/fix-bookmark-folder-tests.skip b/changelog.d/fix-bookmark-folder-tests.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/force-mention-mrf.add b/changelog.d/force-mention-mrf.add new file mode 100644 index 000000000..46ac14244 --- /dev/null +++ b/changelog.d/force-mention-mrf.add @@ -0,0 +1 @@ +Add ForceMention MRF \ No newline at end of file diff --git a/changelog.d/framegrabs.fix b/changelog.d/framegrabs.fix new file mode 100644 index 000000000..dc0466f1b --- /dev/null +++ b/changelog.d/framegrabs.fix @@ -0,0 +1 @@ +Video framegrabs were not working correctly after the change to use Exile to execute ffmpeg diff --git a/changelog.d/instance-contact-account.add b/changelog.d/instance-contact-account.add new file mode 100644 index 000000000..e119446d2 --- /dev/null +++ b/changelog.d/instance-contact-account.add @@ -0,0 +1 @@ +Add contact account to InstanceView \ No newline at end of file diff --git a/changelog.d/issue-3241.fix b/changelog.d/issue-3241.fix new file mode 100644 index 000000000..d46db9805 --- /dev/null +++ b/changelog.d/issue-3241.fix @@ -0,0 +1 @@ +Handle cases when users.inbox is nil. diff --git a/changelog.d/link-verification.add b/changelog.d/link-verification.add new file mode 100644 index 000000000..d8b11ebbc --- /dev/null +++ b/changelog.d/link-verification.add @@ -0,0 +1 @@ +Verify profile link ownership with rel="me" \ No newline at end of file diff --git a/changelog.d/notifications.fix b/changelog.d/notifications.fix new file mode 100644 index 000000000..a2d2eaea9 --- /dev/null +++ b/changelog.d/notifications.fix @@ -0,0 +1 @@ +Notifications: improve performance by filtering on users table instead of activities table \ No newline at end of file diff --git a/changelog.d/postgres-jit.change b/changelog.d/postgres-jit.change new file mode 100644 index 000000000..38225b06b --- /dev/null +++ b/changelog.d/postgres-jit.change @@ -0,0 +1 @@ +Disable jit by default for PostgreSQL diff --git a/changelog.d/public-polls.add b/changelog.d/public-polls.add new file mode 100644 index 000000000..0dae0c38e --- /dev/null +++ b/changelog.d/public-polls.add @@ -0,0 +1 @@ +Expose nonAnonymous field from Smithereen polls \ No newline at end of file diff --git a/changelog.d/receiverworker-error-handling.fix b/changelog.d/receiverworker-error-handling.fix new file mode 100644 index 000000000..f017a2bba --- /dev/null +++ b/changelog.d/receiverworker-error-handling.fix @@ -0,0 +1 @@ +ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …} \ No newline at end of file diff --git a/changelog.d/test-improvements.skip b/changelog.d/test-improvements.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/transient-validators-defaults.change b/changelog.d/transient-validators-defaults.change new file mode 100644 index 000000000..225cf4d0c --- /dev/null +++ b/changelog.d/transient-validators-defaults.change @@ -0,0 +1 @@ +Set default values on validators for transient objects (attachment, poll options) diff --git a/config/config.exs b/config/config.exs index 435387a64..32c8509be 100644 --- a/config/config.exs +++ b/config/config.exs @@ -415,6 +415,10 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil config :pleroma, :mrf_inline_quote, template: "RT: {url}" +config :pleroma, :mrf_force_mention, + mention_parent: true, + mention_quoted: true + config :pleroma, :rich_media, enabled: true, ignore_hosts: [], @@ -795,7 +799,7 @@ config :pleroma, :modules, runtime_dir: "instance/modules" config :pleroma, configurable_from_database: false config :pleroma, Pleroma.Repo, - parameters: [gin_fuzzy_search_limit: "500"], + parameters: [gin_fuzzy_search_limit: "500", jit: "off"], prepare: :unnamed config :pleroma, :connections_pool, diff --git a/config/description.exs b/config/description.exs index b1730bab3..7d012b4b5 100644 --- a/config/description.exs +++ b/config/description.exs @@ -574,6 +574,12 @@ config :pleroma, :config_description, [ "https://status.pleroma.example.org" ] }, + %{ + key: :contact_username, + type: :string, + description: "Instance owner username", + suggestions: ["admin"] + }, %{ key: :limit, type: :integer, diff --git a/config/test.exs b/config/test.exs index 9d752bdf8..80b01932c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -49,7 +49,7 @@ config :pleroma, Pleroma.Repo, hostname: System.get_env("DB_HOST") || "localhost", port: System.get_env("DB_PORT") || "5432", pool: Ecto.Adapters.SQL.Sandbox, - pool_size: 50 + pool_size: System.schedulers_online() * 2 config :pleroma, :dangerzone, override_repo_pool_size: true diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 7bba7b26e..89a461b47 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -161,7 +161,8 @@ To add configuration to your config file, you can copy it from the base config. * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)). * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content. * `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline. - * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions) + * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions). + * `Pleroma.Web.ActivityPub.MRF.ForceMention`: Forces posts to include a mention of the author of parent post or the author of quoted post. * `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. @@ -272,6 +273,10 @@ Notes: #### :mrf_inline_quote * `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `RT: {url}` +#### :mrf_force_mention +* `mention_parent`: Whether to append mention of parent post author +* `mention_quoted`: Whether to append mention of parent quoted author + ### :activitypub * `unfollow_blocked`: Whether blocks result in people getting unfollowed * `outgoing_blocks`: Whether to federate blocks to other instances diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 2937b2301..e3b6a3c77 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -40,6 +40,8 @@ Has these additional fields under the `pleroma` object: - `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. - `quotes_count`: the count of status quotes. +- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen. +- `bookmark_folder`: the ID of the folder bookmark is stored within (if any). The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes: @@ -65,6 +67,12 @@ Some apps operate under the assumption that no more than 4 attachments can be re Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it. +## Bookmarks + +The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID. + +The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. + ## Accounts The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc. diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 060af5c14..267dfc1ec 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -283,6 +283,52 @@ See [Admin-API](admin_api.md) * `id`: the id of the status * Response: JSON, returns a list of Mastodon Status entities +## `GET /api/v1/pleroma/bookmark_folders` +### Gets user bookmark folders +* Authentication: required + +* Response: JSON. Returns a list of bookmark folders. +* Example response: +```json +[ + { + "id": "9umDrYheeY451cQnEe", + "name": "Read later", + "emoji": "🕓", + "source": { + "emoji": "🕓" + } + } +] +``` + +## `POST /api/v1/pleroma/bookmark_folders` +### Creates a bookmark folder +* Authentication: required + +* Params: + * `name`: folder name + * `emoji`: folder emoji (optional) +* Response: JSON. Returns a single bookmark folder. + +## `PATCH /api/v1/pleroma/bookmark_folders/:id` +### Updates a bookmark folder +* Authentication: required + +* Params: + * `id`: folder id + * `name`: folder name (optional) + * `emoji`: folder emoji (optional) +* Response: JSON. Returns a single bookmark folder. + +## `DELETE /api/v1/pleroma/bookmark_folders/:id` +### Deletes a bookmark folder +* Authentication: required + +* Params: + * `id`: folder id +* Response: JSON. Returns a single bookmark folder. + ## `/api/v1/pleroma/mascot` ### Gets user mascot image * Method `GET` diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 1424ad7f4..b6b5c9c07 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -12,8 +12,8 @@ Note: This article is potentially outdated because at this time we may not have ### 必要なソフトウェア -- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) -- `postgresql-contrib` 9.6以上 (同上) +- PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) +- `postgresql-contrib` 11.0以上 (同上) - Elixir 1.8 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) - `erlang-dev` - `erlang-nox` diff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include index aebf21e7c..6572716ed 100644 --- a/docs/installation/generic_dependencies.include +++ b/docs/installation/generic_dependencies.include @@ -1,6 +1,6 @@ ## Required dependencies -* PostgreSQL >=9.6 +* PostgreSQL >=11.0 * Elixir >=1.11.0 <1.15 * Erlang OTP >=22.2.0 (supported: <27) * git diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 921384be9..5e6b227dc 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -119,28 +119,7 @@ defmodule Pleroma.Application do max_restarts = Application.get_env(:pleroma, __MODULE__)[:max_restarts] opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts] - result = Supervisor.start_link(children, opts) - - set_postgres_server_version() - - result - end - - defp set_postgres_server_version do - version = - with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"), - {num, _} <- Float.parse(version) do - num - else - e -> - Logger.warning( - "Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6" - ) - - 9.6 - end - - :persistent_term.put({Pleroma.Repo, :postgres_version}, version) + Supervisor.start_link(children, opts) end def load_custom_modules do @@ -177,6 +156,7 @@ defmodule Pleroma.Application do build_cachex("web_resp", limit: 2500), build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), build_cachex("failed_proxy_url", limit: 2500), + build_cachex("failed_media_helper_url", default_ttl: :timer.minutes(15), limit: 2_500), build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000), build_cachex("chat_message_id_idempotency_key", expiration: chat_message_id_idempotency_key_expiration(), diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex index b83d72446..1a2a63b82 100644 --- a/lib/pleroma/bookmark.ex +++ b/lib/pleroma/bookmark.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Bookmark do alias Pleroma.Activity alias Pleroma.Bookmark + alias Pleroma.BookmarkFolder alias Pleroma.Repo alias Pleroma.User @@ -18,33 +19,46 @@ defmodule Pleroma.Bookmark do schema "bookmarks" do belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) + belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType) timestamps() end @spec create(Ecto.UUID.t(), Ecto.UUID.t()) :: {:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()} - def create(user_id, activity_id) do + def create(user_id, activity_id, folder_id \\ nil) do attrs = %{ user_id: user_id, - activity_id: activity_id + activity_id: activity_id, + folder_id: folder_id } %Bookmark{} - |> cast(attrs, [:user_id, :activity_id]) + |> cast(attrs, [:user_id, :activity_id, :folder_id]) |> validate_required([:user_id, :activity_id]) |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) - |> Repo.insert() + |> Repo.insert( + on_conflict: [set: [folder_id: folder_id]], + conflict_target: [:user_id, :activity_id] + ) end @spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t() - def for_user_query(user_id) do + def for_user_query(user_id, folder_id \\ nil) do Bookmark |> where(user_id: ^user_id) + |> maybe_filter_by_folder(folder_id) |> join(:inner, [b], activity in assoc(b, :activity)) |> preload([b, a], activity: a) end + defp maybe_filter_by_folder(query, nil), do: query + + defp maybe_filter_by_folder(query, folder_id) do + query + |> where(folder_id: ^folder_id) + end + def get(user_id, activity_id) do Bookmark |> where(user_id: ^user_id) @@ -62,4 +76,11 @@ defmodule Pleroma.Bookmark do |> Repo.one() |> Repo.delete() end + + def set_folder(bookmark, folder_id) do + bookmark + |> cast(%{folder_id: folder_id}, [:folder_id]) + |> validate_required([:folder_id]) + |> Repo.update() + end end diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex new file mode 100644 index 000000000..14d37e197 --- /dev/null +++ b/lib/pleroma/bookmark_folder.ex @@ -0,0 +1,115 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.BookmarkFolder do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.BookmarkFolder + alias Pleroma.Emoji + alias Pleroma.Repo + alias Pleroma.User + + @type t :: %__MODULE__{} + @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} + + schema "bookmark_folders" do + field(:name, :string) + field(:emoji, :string) + + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + + timestamps() + end + + def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id) + + def create(user_id, name, emoji \\ nil) do + %BookmarkFolder{} + |> cast( + %{ + user_id: user_id, + name: name, + emoji: emoji + }, + [:user_id, :name, :emoji] + ) + |> validate_required([:user_id, :name]) + |> fix_emoji() + |> validate_emoji() + |> unique_constraint([:user_id, :name]) + |> Repo.insert() + end + + def update(folder_id, name, emoji \\ nil) do + get_by_id(folder_id) + |> cast( + %{ + name: name, + emoji: emoji + }, + [:name, :emoji] + ) + |> fix_emoji() + |> validate_emoji() + |> unique_constraint([:user_id, :name]) + |> Repo.update() + end + + defp fix_emoji(changeset) do + with {:emoji_field, emoji} when is_binary(emoji) <- + {:emoji_field, get_field(changeset, :emoji)}, + {:fixed_emoji, emoji} <- + {:fixed_emoji, + emoji + |> Pleroma.Emoji.fully_qualify_emoji() + |> Pleroma.Emoji.maybe_quote()} do + put_change(changeset, :emoji, emoji) + else + {:emoji_field, _} -> changeset + end + end + + defp validate_emoji(changeset) do + validate_change(changeset, :emoji, fn + :emoji, nil -> + [] + + :emoji, emoji -> + if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do + [] + else + [emoji: "Invalid emoji"] + end + end) + end + + defp valid_local_custom_emoji?(emoji) do + with %{file: _path} <- Emoji.get(emoji) do + true + else + _ -> false + end + end + + def delete(folder_id) do + BookmarkFolder + |> Repo.get_by(id: folder_id) + |> Repo.delete() + end + + def for_user(user_id) do + BookmarkFolder + |> where(user_id: ^user_id) + |> Repo.all() + end + + def belongs_to_user?(folder_id, user_id) do + BookmarkFolder + |> where(id: ^folder_id, user_id: ^user_id) + |> Repo.exists?() + end +end diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 15664c876..f38c2fce9 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -241,13 +241,13 @@ defmodule Pleroma.FollowingRelationship do end @doc """ - For a query with joined activity, - keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user. + For a query with joined activity's actor, + keeps rows where actor is followed by user -or- is NOT domain-blocked by user. """ def keep_following_or_not_domain_blocked(query, user) do where( query, - [_, activity], + [_, user_actor: user_actor], fragment( # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)" """ @@ -255,9 +255,9 @@ defmodule Pleroma.FollowingRelationship do ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?) """, - activity.actor, + user_actor.ap_id, ^user.domain_blocks, - activity.actor, + user_actor.ap_id, ^User.binary_id(user.id), ^accept_state_code() ) diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex index 1a414b37f..e44114d9d 100644 --- a/lib/pleroma/helpers/media_helper.ex +++ b/lib/pleroma/helpers/media_helper.ex @@ -12,6 +12,8 @@ defmodule Pleroma.Helpers.MediaHelper do require Logger + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + def missing_dependencies do Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc -> if Pleroma.Utils.command_available?(executable) do @@ -40,28 +42,43 @@ defmodule Pleroma.Helpers.MediaHelper do end # Note: video thumbnail is intentionally not resized (always has original dimensions) + @spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()} def video_framegrab(url) do with executable when is_binary(executable) <- System.find_executable("ffmpeg"), + false <- @cachex.exists?(:failed_media_helper_cache, url), {:ok, env} <- HTTP.get(url, [], pool: :media), {:ok, pid} <- StringIO.open(env.body) do body_stream = IO.binstream(pid, 1) - Exile.stream!( - [ - executable, - "-i", - "pipe:0", - "-vframes", - "1", - "-f", - "mjpeg", - "pipe:1" - ], - input: body_stream, - ignore_epipe: true, - stderr: :disable - ) - |> Enum.into(<<>>) + task = + Task.async(fn -> + Exile.stream!( + [ + executable, + "-i", + "pipe:0", + "-vframes", + "1", + "-f", + "mjpeg", + "pipe:1" + ], + input: body_stream, + ignore_epipe: true, + stderr: :disable + ) + |> Enum.into(<<>>) + end) + + case Task.yield(task, 5_000) do + nil -> + Task.shutdown(task) + @cachex.put(:failed_media_helper_cache, url, nil) + {:error, {:ffmpeg, :timeout}} + + result -> + {:ok, result} + end else nil -> {:error, {:ffmpeg, :command_not_found}} {:error, _} = error -> error diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 368e609d2..710b19866 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -137,7 +137,7 @@ defmodule Pleroma.Notification do blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) query - |> where([n, a], a.actor not in ^blocked_ap_ids) + |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids) |> FollowingRelationship.keep_following_or_not_domain_blocked(user) end @@ -148,7 +148,7 @@ defmodule Pleroma.Notification do blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block]) query - |> where([n, a], a.actor not in ^blocker_ap_ids) + |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids) end end @@ -161,7 +161,7 @@ defmodule Pleroma.Notification do opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user) query - |> where([n, a], a.actor not in ^notification_muted_ap_ids) + |> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids) |> join(:left, [n, a], tm in ThreadMute, on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data), as: :thread_mute diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex index c6311e0c7..31bfc7e33 100644 --- a/lib/pleroma/search/database_search.ex +++ b/lib/pleroma/search/database_search.ex @@ -23,19 +23,12 @@ defmodule Pleroma.Search.DatabaseSearch do offset = Keyword.get(options, :offset, 0) author = Keyword.get(options, :author) - search_function = - if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do - :websearch - else - :plain - end - try do Activity |> Activity.with_preloaded_object() |> Activity.restrict_deactivated_users() |> restrict_public(user) - |> query_with(index_type, search_query, search_function) + |> query_with(index_type, search_query, :websearch) |> maybe_restrict_local(user) |> maybe_restrict_author(author) |> maybe_restrict_blocked(user) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 0773434c5..778e20526 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -8,6 +8,7 @@ defmodule Pleroma.User do import Ecto.Changeset import Ecto.Query import Ecto, only: [assoc: 2] + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] alias Ecto.Multi alias Pleroma.Activity @@ -596,9 +597,23 @@ defmodule Pleroma.User do defp put_fields(changeset) do if raw_fields = get_change(changeset, :raw_fields) do + old_fields = changeset.data.raw_fields + raw_fields = raw_fields |> Enum.filter(fn %{"name" => n} -> n != "" end) + |> Enum.map(fn field -> + previous = + old_fields + |> Enum.find(fn %{"value" => value} -> field["value"] == value end) + + if previous && Map.has_key?(previous, "verified_at") do + field + |> Map.put("verified_at", previous["verified_at"]) + else + field + end + end) fields = raw_fields @@ -1200,6 +1215,10 @@ defmodule Pleroma.User do def update_and_set_cache(changeset) do with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do + if get_change(changeset, :raw_fields) do + BackgroundWorker.enqueue("verify_fields_links", %{"user_id" => user.id}) + end + set_cache(user) end end @@ -1975,8 +1994,45 @@ defmodule Pleroma.User do maybe_delete_from_db(user) end + def perform(:verify_fields_links, user) do + profile_urls = [user.ap_id] + + fields = + user.raw_fields + |> Enum.map(&verify_field_link(&1, profile_urls)) + + changeset = + user + |> update_changeset(%{raw_fields: fields}) + + with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do + set_cache(user) + end + end + def perform(:set_activation_async, user, status), do: set_activation(user, status) + defp verify_field_link(field, profile_urls) do + verified_at = + with %{"value" => value} <- field, + {:verified_at, nil} <- {:verified_at, Map.get(field, "verified_at")}, + %{scheme: scheme, userinfo: nil, host: host} + when not_empty_string(host) and scheme in ["http", "https"] <- + URI.parse(value), + {:not_idn, true} <- {:not_idn, to_string(:idna.encode(host)) == host}, + "me" <- Pleroma.Web.RelMe.maybe_put_rel_me(value, profile_urls) do + CommonUtils.to_masto_date(NaiveDateTime.utc_now()) + else + {:verified_at, value} when not_empty_string(value) -> + value + + _ -> + nil + end + + Map.put(field, "verified_at", verified_at) + end + @spec external_users_query() :: Ecto.Query.t() def external_users_query do User.Query.build(%{ @@ -2664,10 +2720,11 @@ defmodule Pleroma.User do # - display name def sanitize_html(%User{} = user, filter) do fields = - Enum.map(user.fields, fn %{"name" => name, "value" => value} -> + Enum.map(user.fields, fn %{"name" => name, "value" => value} = fields -> %{ "name" => name, - "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly) + "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly), + "verified_at" => Map.get(fields, "verified_at") } end) diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index b7f00bbf7..65e0baccd 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -196,7 +196,14 @@ defmodule Pleroma.User.Backup do end end - @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] + @files [ + 'actor.json', + 'outbox.json', + 'likes.json', + 'bookmarks.json', + 'followers.json', + 'following.json' + ] @spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error def export(%__MODULE__{} = backup, caller_pid) do backup = Repo.preload(backup, :user) @@ -207,6 +214,8 @@ defmodule Pleroma.User.Backup do :ok <- statuses(dir, backup.user, caller_pid), :ok <- likes(dir, backup.user, caller_pid), :ok <- bookmarks(dir, backup.user, caller_pid), + :ok <- followers(dir, backup.user, caller_pid), + :ok <- following(dir, backup.user, caller_pid), {:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir), {:ok, _} <- File.rm_rf(dir) do {:ok, zip_path} @@ -357,6 +366,16 @@ defmodule Pleroma.User.Backup do caller_pid ) end + + defp followers(dir, user, caller_pid) do + User.get_followers_query(user) + |> write(dir, "followers", fn a -> {:ok, a.ap_id} end, caller_pid) + end + + defp following(dir, user, caller_pid) do + User.get_friends_query(user) + |> write(dir, "following", fn a -> {:ok, a.ap_id} end, caller_pid) + end end defmodule Pleroma.User.Backup.ProcessorAPI do diff --git a/lib/pleroma/web/activity_pub/mrf/force_mention.ex b/lib/pleroma/web/activity_pub/mrf/force_mention.ex new file mode 100644 index 000000000..3853489fc --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/force_mention.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do + require Pleroma.Constants + + alias Pleroma.Config + alias Pleroma.Object + alias Pleroma.User + + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + defp get_author(url) do + with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false), + %User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do + %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"} + else + _ -> nil + end + end + + defp prepend_author(tags, _, false), do: tags + + defp prepend_author(tags, nil, _), do: tags + + defp prepend_author(tags, url, _) do + actor = get_author(url) + + if not is_nil(actor) do + [actor | tags] + else + tags + end + end + + @impl true + def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do + tag = + tag + |> prepend_author( + object["inReplyTo"], + Config.get([:mrf_force_mention, :mention_parent, true]) + ) + |> prepend_author( + object["quoteUrl"], + Config.get([:mrf_force_mention, :mention_quoted, true]) + ) + |> Enum.uniq() + + {:ok, put_in(activity["object"]["tag"], tag)} + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} +end diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex index 398020bff..72975f348 100644 --- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -12,13 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do @primary_key false embedded_schema do field(:id, :string) - field(:type, :string) + field(:type, :string, default: "Link") field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream") field(:name, :string) field(:blurhash, :string) embeds_many :url, UrlObjectValidator, primary_key: false do - field(:type, :string) + field(:type, :string, default: "Link") field(:href, ObjectValidators.Uri) field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream") field(:width, :integer) diff --git a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex index 541945fa4..8d7f7b9fa 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex @@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do embeds_one :replies, Replies, primary_key: false do field(:totalItems, :integer) - field(:type, :string) + field(:type, :string, default: "Collection") end - field(:type, :string) + field(:type, :string, default: "Note") end def changeset(struct, data) do diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex index 621085e6c..7f9d4d648 100644 --- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex @@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do field(:closed, ObjectValidators.DateTime) field(:voters, {:array, ObjectValidators.ObjectID}, default: []) + field(:nonAnonymous, :boolean) embeds_many(:anyOf, QuestionOptionsValidator) embeds_many(:oneOf, QuestionOptionsValidator) end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 9e7d00519..a42b4844e 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -158,19 +158,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end end - defp should_federate?(inbox, public) do - if public do - true - else - %{host: host} = URI.parse(inbox) + def should_federate?(nil, _), do: false + def should_federate?(_, true), do: true - quarantined_instances = - Config.get([:instance, :quarantined_instances], []) - |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples() - |> Pleroma.Web.ActivityPub.MRF.subdomains_regex() + def should_federate?(inbox, _) do + %{host: host} = URI.parse(inbox) - !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host) - end + quarantined_instances = + Config.get([:instance, :quarantined_instances], []) + |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples() + |> Pleroma.Web.ActivityPub.MRF.subdomains_regex() + + !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host) end @spec recipients(User.t(), Activity.t()) :: [[User.t()]] diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 24ee683ae..937e4fd67 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -67,8 +67,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("user.json", %{user: %User{nickname: nil} = user}), do: render("service.json", %{user: user}) - def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), - do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) + def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do + render("service.json", %{user: user}) + |> Map.merge(%{ + "preferredUsername" => user.nickname, + "webfinger" => "acct:#{User.full_nickname(user)}" + }) + end def render("user.json", %{user: user}) do {:ok, _, public_key} = Keys.keys_from_pem(user.keys) @@ -121,7 +126,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do "discoverable" => user.is_discoverable, "capabilities" => capabilities, "alsoKnownAs" => user.also_known_as, - "vcard:bday" => birthday + "vcard:bday" => birthday, + "webfinger" => "acct:#{User.full_nickname(user)}" } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 3588608f2..10d221571 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -137,7 +137,8 @@ defmodule Pleroma.Web.ApiSpec do "Scheduled statuses", "Search", "Status actions", - "Media attachments" + "Media attachments", + "Bookmark folders" ] }, %{ diff --git a/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex new file mode 100644 index 000000000..eaa683125 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex @@ -0,0 +1,125 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BookmarkFolder + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + import Pleroma.Web.ApiSpec.Helpers + + @spec open_api_operation(any()) :: any() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Bookmark folders"], + summary: "All bookmark folders", + security: [%{"oAuth" => ["read:bookmarks"]}], + operationId: "PleromaAPI.BookmarkFolderController.index", + responses: %{ + 200 => + Operation.response("Array of Bookmark Folders", "application/json", %Schema{ + type: :array, + items: BookmarkFolder + }) + } + } + end + + def create_operation do + %Operation{ + tags: ["Bookmark folders"], + summary: "Create a bookmark folder", + security: [%{"oAuth" => ["write:bookmarks"]}], + operationId: "PleromaAPI.BookmarkFolderController.create", + requestBody: request_body("Parameters", create_request(), required: true), + responses: %{ + 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder), + 422 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Bookmark folders"], + summary: "Update a bookmark folder", + security: [%{"oAuth" => ["write:bookmarks"]}], + operationId: "PleromaAPI.BookmarkFolderController.update", + parameters: [id_param()], + requestBody: request_body("Parameters", update_request(), required: true), + responses: %{ + 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError), + 422 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Bookmark folders"], + summary: "Delete a bookmark folder", + security: [%{"oAuth" => ["write:bookmarks"]}], + operationId: "PleromaAPI.BookmarkFolderController.delete", + parameters: [id_param()], + responses: %{ + 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + defp create_request do + %Schema{ + title: "BookmarkFolderCreateRequest", + type: :object, + properties: %{ + name: %Schema{ + type: :string, + description: "Folder name" + }, + emoji: %Schema{ + type: :string, + nullable: true, + description: "Folder emoji" + } + } + } + end + + defp update_request do + %Schema{ + title: "BookmarkFolderUpdateRequest", + type: :object, + properties: %{ + name: %Schema{ + type: :string, + nullable: true, + description: "Folder name" + }, + emoji: %Schema{ + type: :string, + nullable: true, + description: "Folder emoji" + } + } + } + end + + def id_param do + Operation.parameter(:id, :path, FlakeID.schema(), "Bookmark Folder ID", + example: "9umDrYheeY451cQnEe", + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 00529bc47..d87d59ef6 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -256,6 +256,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do description: "Privately bookmark a status", operationId: "StatusController.bookmark", parameters: [id_param()], + requestBody: + request_body("Parameters", %Schema{ + title: "StatusUpdateRequest", + type: :object, + properties: %{ + folder_id: %Schema{ + nullable: true, + allOf: [FlakeID], + description: "ID of bookmarks folder, if any" + } + } + }), responses: %{ 200 => status_response() } @@ -462,7 +474,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do summary: "Bookmarked statuses", description: "Statuses the user has bookmarked", operationId: "StatusController.bookmarks", - parameters: pagination_params(), + parameters: [ + Operation.parameter( + :folder_id, + :query, + FlakeID.schema(), + "If provided, only display bookmarks from given folder" + ) + | pagination_params() + ], security: [%{"oAuth" => ["read:bookmarks"]}], responses: %{ 200 => Operation.response("Array of Statuses", "application/json", array_of_statuses()) diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex new file mode 100644 index 000000000..e8b4f43b7 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "BookmarkFolder", + description: "Response schema for a bookmark folder", + type: :object, + properties: %{ + id: FlakeID, + name: %Schema{type: :string, description: "Folder name"}, + emoji: %Schema{type: :string, description: "Folder emoji", nullable: true} + }, + example: %{ + "id" => "9toJCu5YZW7O7gfvH6", + "name" => "Read later", + "emoji" => nil + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex index 91570582b..20cf5b061 100644 --- a/lib/pleroma/web/api_spec/schemas/poll.ex +++ b/lib/pleroma/web/api_spec/schemas/poll.ex @@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do } }, description: "Possible answers for the poll." + }, + pleroma: %Schema{ + type: :object, + properties: %{ + non_anonymous: %Schema{ + type: :boolean, + description: "Can voters be publicly identified?" + } + } } }, example: %{ @@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do votes_count: 4 } ], - emojis: [] + emojis: [], + pleroma: %{ + non_anonymous: false + } } }) end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index a4052803b..6e537b5da 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do format: :uri, description: "Preview thumbnail" }, + image_description: %Schema{ + type: :string, + description: "Alternate text that describes what is in the thumbnail" + }, title: %Schema{type: :string, description: "Title of linked resource"}, description: %Schema{type: :string, description: "Description of preview"} } diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index ad1e78c30..9344b346a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark + alias Pleroma.BookmarkFolder alias Pleroma.Language.Translation alias Pleroma.Object alias Pleroma.Repo @@ -414,13 +415,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "POST /api/v1/statuses/:id/bookmark" def bookmark( - %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, + %{ + assigns: %{user: user}, + private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}} + } = conn, _ ) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), - {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do + folder_id <- Map.get(body_params, :folder_id, nil), + folder_id <- + if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id), + do: folder_id, + else: nil + ), + {:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do try_render(conn, "show.json", activity: activity, for: user, as: :activity) end end @@ -611,10 +621,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "GET /api/v1/bookmarks" def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do user = User.get_cached_by_id(user.id) + folder_id = Map.get(params, :folder_id) bookmarks = user.id - |> Bookmark.for_user_query() + |> Bookmark.for_user_query(folder_id) |> Pleroma.Pagination.fetch_paginated(params) activities = diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index e093b1a2f..229679c39 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -28,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do |> to_string, registrations: Keyword.get(instance, :registrations_open), approval_required: Keyword.get(instance, :account_approval_required), + contact_account: contact_account(Keyword.get(instance, :contact_username)), configuration: configuration(), # Extra (not present in Mastodon): max_toot_chars: Keyword.get(instance, :limit), @@ -68,7 +69,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }, contact: %{ email: Keyword.get(instance, :email), - account: nil + account: contact_account(Keyword.get(instance, :contact_username)) }, # Extra (not present in Mastodon): pleroma: pleroma_configuration2(instance) @@ -139,6 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do end, "pleroma:get:main/ostatus", "pleroma:group_actors", + "pleroma:bookmark_folders", if Pleroma.Language.Translation.configured?() do "translation" end @@ -182,6 +184,22 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do } end + defp contact_account(nil), do: nil + + defp contact_account("@" <> username) do + contact_account(username) + end + + defp contact_account(username) do + user = Pleroma.User.get_cached_by_nickname(username) + + if user do + Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil}) + else + nil + end + end + defp configuration do %{ accounts: %{ diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex index 34e23873e..1e3c9f36d 100644 --- a/lib/pleroma/web/mastodon_api/views/poll_view.ex +++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex @@ -21,7 +21,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do votes_count: votes_count, voters_count: voters_count(object), options: options, - emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) + emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]), + pleroma: %{ + non_anonymous: object.data["nonAnonymous"] || false + } } if params[:for] do diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 026429d56..2bd857607 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -184,7 +184,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil + bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) + + bookmark_folder = + if bookmark != nil do + bookmark.folder_id + else + nil + end mentions = activity.recipients @@ -213,7 +220,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do favourites_count: 0, reblogged: reblogged?(reblogged_parent_activity, opts[:for]), favourited: present?(favorited), - bookmarked: present?(bookmarked), + bookmarked: present?(bookmark), muted: false, pinned: pinned?, sensitive: false, @@ -227,7 +234,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do emojis: [], pleroma: %{ local: activity.local, - pinned_at: pinned_at + pinned_at: pinned_at, + bookmark_folder: bookmark_folder } } end @@ -264,7 +272,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil + bookmark = Activity.get_bookmark(activity, opts[:for]) + + bookmark_folder = + if bookmark != nil do + bookmark.folder_id + else + nil + end client_posted_this_activity = opts[:for] && user.id == opts[:for].id @@ -418,7 +433,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do favourites_count: like_count, reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), - bookmarked: present?(bookmarked), + bookmarked: present?(bookmark), muted: muted, pinned: pinned?, sensitive: sensitive, @@ -448,7 +463,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do emoji_reactions: emoji_reactions, parent_visible: visible_for_user?(reply_to, opts[:for]), pinned_at: pinned_at, - quotes_count: object.data["quotesCount"] || 0 + quotes_count: object.data["quotesCount"] || 0, + bookmark_folder: bookmark_folder } } end @@ -573,6 +589,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do provider_url: page_url_data.scheme <> "://" <> page_url_data.host, url: page_url, image: image_url, + image_description: rich_media["image:alt"] || "", title: rich_media["title"] || "", description: rich_media["description"] || "", pleroma: %{ diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex new file mode 100644 index 000000000..6d6e2e940 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do + use Pleroma.Web, :controller + + alias Pleroma.BookmarkFolder + alias Pleroma.Web.Plugs.OAuthScopesPlug + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + # Note: scope not present in Mastodon: read:bookmarks + plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index) + + # Note: scope not present in Mastodon: write:bookmarks + plug( + OAuthScopesPlug, + %{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + def index(%{assigns: %{user: user}} = conn, _params) do + with folders <- BookmarkFolder.for_user(user.id) do + conn + |> render("index.json", %{folders: folders, as: :folder}) + end + end + + def create( + %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn, + _ + ) do + with {:ok, folder} <- BookmarkFolder.create(user.id, params[:name], params[:emoji]) do + render(conn, "show.json", folder: folder) + end + end + + def update( + %{ + assigns: %{user: user}, + private: %{open_api_spex: %{body_params: params, params: %{id: id}}} + } = conn, + _ + ) do + with true <- BookmarkFolder.belongs_to_user?(id, user.id), + {:ok, folder} <- BookmarkFolder.update(id, params[:name], params[:emoji]) do + render(conn, "show.json", folder: folder) + else + false -> {:error, :forbidden} + end + end + + def delete( + %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, + _ + ) do + with true <- BookmarkFolder.belongs_to_user?(id, user.id), + {:ok, folder} <- BookmarkFolder.delete(id) do + render(conn, "show.json", folder: folder) + else + false -> {:error, :forbidden} + end + end +end diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex new file mode 100644 index 000000000..12decb816 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do + use Pleroma.Web, :view + + alias Pleroma.BookmarkFolder + alias Pleroma.Emoji + alias Pleroma.Web.Endpoint + + def render("show.json", %{folder: %BookmarkFolder{} = folder}) do + %{ + id: folder.id |> to_string(), + name: folder.name, + emoji: folder.emoji, + emoji_url: get_emoji_url(folder.emoji) + } + end + + def render("index.json", %{folders: folders} = opts) do + render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders)) + end + + defp get_emoji_url(nil) do + nil + end + + defp get_emoji_url(emoji) do + if Emoji.unicode?(emoji) do + nil + else + emoji = Emoji.get(emoji) + + if emoji != nil do + Endpoint.url() |> URI.merge(emoji.file) |> to_string() + else + nil + end + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index fcf6edfd7..4b5c7ca86 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -580,6 +580,11 @@ defmodule Pleroma.Web.Router do get("/backups", BackupController, :index) post("/backups", BackupController, :create) + + get("/bookmark_folders", BookmarkFolderController, :index) + post("/bookmark_folders", BookmarkFolderController, :create) + patch("/bookmark_folders/:id", BookmarkFolderController, :update) + delete("/bookmark_folders/:id", BookmarkFolderController, :delete) end scope [] do diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 7a2210dc1..dbf40ee1b 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -40,6 +40,11 @@ defmodule Pleroma.Workers.BackgroundWorker do Pleroma.FollowingRelationship.move_following(origin, target) end + def perform(%Job{args: %{"op" => "verify_fields_links", "user_id" => user_id}}) do + user = User.get_by_id(user_id) + User.perform(:verify_fields_links, user) + end + def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do Instance.perform(:delete_instance, host) end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 1dddd8d2e..8b2052c23 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -52,7 +52,8 @@ defmodule Pleroma.Workers.ReceiverWorker do {:error, {:reject, reason}} -> {:cancel, reason} {:signature, false} -> {:cancel, :invalid_signature} {:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason} - e -> e + {:error, _} = e -> e + e -> {:error, e} end end end diff --git a/priv/repo/migrations/20240223165000_create_bookmark_folders.exs b/priv/repo/migrations/20240223165000_create_bookmark_folders.exs new file mode 100644 index 000000000..016916968 --- /dev/null +++ b/priv/repo/migrations/20240223165000_create_bookmark_folders.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateBookmarkFolders do + use Ecto.Migration + + def change do + create_if_not_exists table(:bookmark_folders, primary_key: false) do + add(:id, :uuid, primary_key: true) + add(:name, :string, null: false) + add(:emoji, :string) + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + + timestamps() + end + + alter table(:bookmarks) do + add_if_not_exists( + :folder_id, + references(:bookmark_folders, type: :uuid, on_delete: :nilify_all) + ) + end + + create_if_not_exists(unique_index(:bookmark_folders, [:user_id, :name])) + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index b499a96f5..3569165a4 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -2,6 +2,7 @@ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", + "https://purl.archive.org/socialweb/webfinger", { "Emoji": "toot:Emoji", "Hashtag": "as:Hashtag", @@ -40,7 +41,9 @@ "@type": "@id" }, "vcard": "http://www.w3.org/2006/vcard/ns#", - "formerRepresentations": "litepub:formerRepresentations" + "formerRepresentations": "litepub:formerRepresentations", + "sm": "http://smithereen.software/ns#", + "nonAnonymous": "sm:nonAnonymous" } ] } diff --git a/test/fixtures/minds-invalid-mention-post.json b/test/fixtures/minds-invalid-mention-post.json new file mode 100644 index 000000000..ea2cb2739 --- /dev/null +++ b/test/fixtures/minds-invalid-mention-post.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}} \ No newline at end of file diff --git a/test/fixtures/minds-pleroma-mentioned-post.json b/test/fixtures/minds-pleroma-mentioned-post.json new file mode 100644 index 000000000..9dfa42c90 --- /dev/null +++ b/test/fixtures/minds-pleroma-mentioned-post.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json b/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json new file mode 100644 index 000000000..2b343ea64 --- /dev/null +++ b/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json @@ -0,0 +1 @@ +{"type":"Question","id":"https://friends.grishka.me/posts/54642","attributedTo":"https://friends.grishka.me/users/1","content":"

здесь тоже можно что-то написать отдельно от опроса

","published":"2021-09-04T00:22:16Z","url":"https://friends.grishka.me/posts/54642","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://friends.grishka.me/users/1/followers"],"replies":{"type":"Collection","id":"https://friends.grishka.me/posts/54642/replies","first":{"type":"CollectionPage","items":[],"partOf":"https://friends.grishka.me/posts/54642/replies","next":"https://friends.grishka.me/posts/54642/replies?page=1"}},"sensitive":false,"likes":"https://friends.grishka.me/posts/54642/likes","name":"тестовый опрос","oneOf":[{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/76","name":"тестовый ответ 1","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/76/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/77","name":"тестовый ответ 2","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/77/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/78","name":"тестовый ответ 3","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/78/votes","totalItems":6,"items":[]}}],"votersCount":14,"nonAnonymous":true,"@context":["https://www.w3.org/ns/activitystreams",{"sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#","sm":"http://smithereen.software/ns#","votersCount":"toot:votersCount","nonAnonymous":"sm:nonAnonymous"}]} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/smithereen_user.json b/test/fixtures/tesla_mock/smithereen_user.json new file mode 100644 index 000000000..6468fc519 --- /dev/null +++ b/test/fixtures/tesla_mock/smithereen_user.json @@ -0,0 +1 @@ +{"type":"Person","id":"https://friends.grishka.me/users/1","name":"Григорий Клюшников","icon":{"type":"Image","image":{"type":"Image","url":"https://friends.grishka.me/i/6QLsOws97AWp5N_osd74C1IC1ijnFopyCBD9MSEeXNQ/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg","width":1280,"height":960},"width":573,"height":572,"cropRegion":[0.26422762870788574,0.3766937553882599,0.7113820910453796,0.9728997349739075],"url":"https://friends.grishka.me/i/ql_49PQcETAWgY_nC-Qj63H_Oa6FyOAEoWFkUSSkUvQ/c:573:572:nowe:338:362/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg"},"summary":"

Делаю эту хрень, пытаюсь вырвать социальные сети из жадных лап корпораций

\n

\n

\n

\n

\n

\n

\n

\n

This server does NOT support direct messages. Please write me on Telegram or Matrix.

","url":"https://friends.grishka.me/grishka","preferredUsername":"grishka","inbox":"https://friends.grishka.me/users/1/inbox","outbox":"https://friends.grishka.me/users/1/outbox","followers":"https://friends.grishka.me/users/1/followers","following":"https://friends.grishka.me/users/1/following","endpoints":{"sharedInbox":"https://friends.grishka.me/activitypub/sharedInbox","collectionSimpleQuery":"https://friends.grishka.me/users/1/collectionQuery"},"publicKey":{"id":"https://friends.grishka.me/users/1#main-key","owner":"https://friends.grishka.me/users/1","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjlakm+i/d9ER/hIeR7KfiFW+SdLZj2SkKIeM8cmR+YFJuh9ghFqXrkFEjcaqUnAFqe5gYDNSQACnDLA8y4DnzjfGNIohKAnRoa9x6GORmfKQvcnjaTZ53S1NvUiPPyc0Pv/vfCtY/Ab0CEXe5BLqL38oZn817Jf7pBrPRTYH7m012kvwAUTT6k0Y8lPITBEG7nzYbbuGcrN9Y/RDdwE08jmBXlZ45bahRH3VNXVpQE17dCzJB+7k+iJ1R7YCoI+DuMlBYGXGE2KVk46NZTuLnOjFV9SyXfWX4/SrJM4oxev+SX2N75tQgmNZmVVHeqg2ZcbC0WCfNjJOi2HHS9MujwIDAQAB\n-----END PUBLIC KEY-----\n"},"wall":"https://friends.grishka.me/users/1/wall","firstName":"Григорий","lastName":"Клюшников","middleName":"Александрович","vcard:bday":"1993-01-22","gender":"http://schema.org#Male","supportsFriendRequests":true,"friends":"https://friends.grishka.me/users/1/friends","groups":"https://friends.grishka.me/users/1/groups","capabilities":{"supportsFriendRequests":true},"@context":["https://www.w3.org/ns/activitystreams",{"sm":"http://smithereen.software/ns#","cropRegion":{"@id":"sm:cropRegion","@container":"@list"},"wall":{"@id":"sm:wall","@type":"@id"},"collectionSimpleQuery":"sm:collectionSimpleQuery","sc":"http://schema.org#","firstName":"sc:givenName","lastName":"sc:familyName","middleName":"sc:additionalName","gender":{"@id":"sc:gender","@type":"sc:GenderType"},"maidenName":"sm:maidenName","friends":{"@id":"sm:friends","@type":"@id"},"groups":{"@id":"sm:groups","@type":"@id"},"vcard":"http://www.w3.org/2006/vcard/ns#","capabilities":"litepub:capabilities","supportsFriendRequests":"sm:supportsFriendRequests","litepub":"http://litepub.social/ns#"},"https://w3id.org/security/v1"]} \ No newline at end of file diff --git a/test/pleroma/bookmark_folder_test.exs b/test/pleroma/bookmark_folder_test.exs new file mode 100644 index 000000000..c45129b0e --- /dev/null +++ b/test/pleroma/bookmark_folder_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.BookmarkFolderTest do + use Pleroma.DataCase, async: true + import Pleroma.Factory + alias Pleroma.BookmarkFolder + + describe "create/3" do + test "with valid params" do + user = insert(:user) + {:ok, folder} = BookmarkFolder.create(user.id, "Read later", "🕓") + assert folder.user_id == user.id + assert folder.name == "Read later" + assert folder.emoji == "🕓" + end + + test "with invalid params" do + {:error, changeset} = BookmarkFolder.create(nil, "", "not an emoji") + refute changeset.valid? + + assert changeset.errors == [ + emoji: {"Invalid emoji", []}, + user_id: {"can't be blank", [validation: :required]}, + name: {"can't be blank", [validation: :required]} + ] + end + end + + test "update/3" do + user = insert(:user) + {:ok, folder} = BookmarkFolder.create(user.id, "Read ltaer") + {:ok, folder} = BookmarkFolder.update(folder.id, "Read later") + assert folder.name == "Read later" + end + + test "for_user/1" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _} = BookmarkFolder.create(user.id, "Folder 1") + {:ok, _} = BookmarkFolder.create(user.id, "Folder 2") + {:ok, _} = BookmarkFolder.create(other_user.id, "Folder 3") + + folders = BookmarkFolder.for_user(user.id) + + assert length(folders) == 2 + end + + test "belongs_to_user?/2" do + user = insert(:user) + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(user.id, "Folder") + + assert true == BookmarkFolder.belongs_to_user?(folder.id, user.id) + assert false == BookmarkFolder.belongs_to_user?(folder.id, other_user.id) + end +end diff --git a/test/pleroma/bookmark_test.exs b/test/pleroma/bookmark_test.exs index 57144ded3..a2ed24c26 100644 --- a/test/pleroma/bookmark_test.exs +++ b/test/pleroma/bookmark_test.exs @@ -6,15 +6,17 @@ defmodule Pleroma.BookmarkTest do use Pleroma.DataCase, async: true import Pleroma.Factory alias Pleroma.Bookmark + alias Pleroma.BookmarkFolder alias Pleroma.Web.CommonAPI - describe "create/2" do + describe "create/3" do test "with valid params" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) {:ok, bookmark} = Bookmark.create(user.id, activity.id) assert bookmark.user_id == user.id assert bookmark.activity_id == activity.id + assert bookmark.folder_id == nil end test "with invalid params" do @@ -26,6 +28,19 @@ defmodule Pleroma.BookmarkTest do activity_id: {"can't be blank", [validation: :required]} ] end + + test "update existing bookmark folder" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) + + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark.folder_id == nil + + {:ok, bookmark_folder} = BookmarkFolder.create(user.id, "Read later") + + {:ok, bookmark} = Bookmark.create(user.id, activity.id, bookmark_folder.id) + assert bookmark.folder_id == bookmark_folder.id + end end describe "destroy/2" do diff --git a/test/pleroma/search/database_search_test.exs b/test/pleroma/search/database_search_test.exs index 6c47ff425..d8dd09063 100644 --- a/test/pleroma/search/database_search_test.exs +++ b/test/pleroma/search/database_search_test.exs @@ -35,21 +35,6 @@ defmodule Pleroma.Search.DatabaseSearchTest do assert [] = Search.search(nil, "wednesday") end - test "using plainto_tsquery on postgres < 11" do - old_version = :persistent_term.get({Pleroma.Repo, :postgres_version}) - :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0) - on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end) - - user = insert(:user) - {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) - {:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"}) - - # plainto doesn't understand complex queries - assert [result] = Search.search(nil, "wednesday -dudes") - - assert result.id == post.id - end - test "using websearch_to_tsquery" do user = insert(:user) {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs index 0ac57e334..5503d15bc 100644 --- a/test/pleroma/user/backup_test.exs +++ b/test/pleroma/user/backup_test.exs @@ -166,6 +166,7 @@ defmodule Pleroma.User.BackupTest do test "it creates a zip archive with user data" do user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + %{ap_id: other_ap_id} = other_user = insert(:user) {:ok, %{object: %{data: %{"id" => id1}}} = status1} = CommonAPI.post(user, %{status: "status1"}) @@ -182,6 +183,8 @@ defmodule Pleroma.User.BackupTest do Bookmark.create(user.id, status2.id) Bookmark.create(user.id, status3.id) + CommonAPI.follow(user, other_user) + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() assert {:ok, path} = Backup.export(backup, self()) assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory]) @@ -261,6 +264,16 @@ defmodule Pleroma.User.BackupTest do "type" => "OrderedCollection" } = Jason.decode!(json) + assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile) + + assert %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "following.json", + "orderedItems" => [^other_ap_id], + "totalItems" => 1, + "type" => "OrderedCollection" + } = Jason.decode!(json) + :zip.zip_close(zipfile) File.rm!(path) end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 15809ad63..a93f81659 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2928,4 +2928,51 @@ defmodule Pleroma.UserTest do refute User.endorses?(user, pinned_user) end end + + test "it checks fields links for a backlink" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + fields = [ + %{"name" => "Link", "value" => "http://example.com/rel_me/null"}, + %{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}, + %{"name" => "Not a link", "value" => "i'm not a link"} + ] + + user + |> User.update_and_set_cache(%{raw_fields: fields}) + + ObanHelpers.perform_all() + + user = User.get_cached_by_id(user.id) + + assert [ + %{"verified_at" => nil}, + %{"verified_at" => verified_at}, + %{"verified_at" => nil} + ] = user.fields + + assert is_binary(verified_at) + end + + test "updating fields does not invalidate previously validated links" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + user + |> User.update_and_set_cache(%{ + raw_fields: [%{"name" => "verified link", "value" => "http://example.com/rel_me/link"}] + }) + + ObanHelpers.perform_all() + + %User{fields: [%{"verified_at" => verified_at}]} = user = User.get_cached_by_id(user.id) + + user + |> User.update_and_set_cache(%{ + raw_fields: [%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}] + }) + + user = User.get_cached_by_id(user.id) + + assert [%{"verified_at" => ^verified_at}] = user.fields + end end diff --git a/test/pleroma/web/activity_pub/mrf/force_mention_test.exs b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs new file mode 100644 index 000000000..b026bab66 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionTest do + use Pleroma.DataCase + require Pleroma.Constants + + alias Pleroma.Web.ActivityPub.MRF.ForceMention + + import Pleroma.Factory + + test "adds mention to a reply" do + lain = + insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false) + + niobleoum = + insert(:user, + ap_id: "https://www.minds.com/api/activitypub/users/1198929502760083472", + nickname: "niobleoum@minds.com", + local: false + ) + + status = File.read!("test/fixtures/minds-pleroma-mentioned-post.json") |> Jason.decode!() + + status_activity = %{ + "type" => "Create", + "actor" => lain.ap_id, + "object" => status + } + + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity) + + reply = File.read!("test/fixtures/minds-invalid-mention-post.json") |> Jason.decode!() + + reply_activity = %{ + "type" => "Create", + "actor" => niobleoum.ap_id, + "object" => reply + } + + {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(reply_activity) + + assert Enum.find(tag, fn %{"href" => href} -> href == lain.ap_id end) + end + + test "adds mention to a quote" do + user1 = insert(:user, ap_id: "https://misskey.io/users/83ssedkv53") + user2 = insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + status = File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json") |> Jason.decode!() + + status_activity = %{ + "type" => "Create", + "actor" => user1.ap_id, + "object" => status + } + + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity) + + quote_post = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + quote_activity = %{ + "type" => "Create", + "actor" => user2.ap_id, + "object" => quote_post + } + + {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(quote_activity) + + assert Enum.find(tag, fn %{"href" => href} -> href == user1.ap_id end) + end +end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 7aa06a5c4..870f1f77a 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -25,6 +25,17 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do setup_all do: clear_config([:instance, :federating], true) + describe "should_federate?/1" do + test "it returns false when the inbox is nil" do + refute Publisher.should_federate?(nil, false) + refute Publisher.should_federate?(nil, true) + end + + test "it returns true when public is true" do + assert Publisher.should_federate?(false, true) + end + end + describe "gather_webfinger_links/1" do test "it returns links" do user = insert(:user) @@ -205,6 +216,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do refute called(Instances.set_reachable(inbox)) end + @tag capture_log: true test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", Instances, [:passthrough], diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index f9068db07..c75149dab 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -91,6 +91,13 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do assert %{"alsoKnownAs" => ^akas} = UserView.render("user.json", %{user: user}) end + test "renders full nickname" do + clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev") + + user = insert(:user, nickname: "user") + assert %{"webfinger" => "acct:user@plemora.dev"} = UserView.render("user.json", %{user: user}) + end + describe "endpoints" do test "local users have a usable endpoints structure" do user = insert(:user) diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index 8ffcff9f3..f91c0283b 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -107,6 +107,18 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do |> json_response_and_validate_schema(200) end + test "get instance contact information", %{conn: conn} do + user = insert(:user, %{local: true}) + + clear_config([:instance, :contact_username], user.nickname) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + + assert result["contact_account"]["id"] == user.id + end + test "get instance information v2", %{conn: conn} do clear_config([:auth, :oauth_consumer_strategies], []) diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index b05487abe..ad4144da4 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -322,26 +322,20 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do end test "search fetches remote statuses and prefers them over other results", %{conn: conn} do - old_version = :persistent_term.get({Pleroma.Repo, :postgres_version}) - :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0) - on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end) + {:ok, %{id: activity_id}} = + CommonAPI.post(insert(:user), %{ + status: "check out http://mastodon.example.org/@admin/99541947525187367" + }) - capture_log(fn -> - {:ok, %{id: activity_id}} = - CommonAPI.post(insert(:user), %{ - status: "check out http://mastodon.example.org/@admin/99541947525187367" - }) + %{"url" => result_url, "id" => result_id} = + conn + |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") + |> json_response_and_validate_schema(200) + |> Map.get("statuses") + |> List.first() - results = - conn - |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") - |> json_response_and_validate_schema(200) - - assert [ - %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, - %{"id" => ^activity_id} - ] = results["statuses"] - end) + refute match?(^result_id, activity_id) + assert match?(^result_url, "http://mastodon.example.org/@admin/99541947525187367") end test "search doesn't show statuses that it shouldn't", %{conn: conn} do diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index 2a64cac5f..003619a38 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -1717,6 +1717,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do card_data = %{ "image" => "http://ia.media-imdb.com/images/rock.jpg", + "image_description" => "", "provider_name" => "example.com", "provider_url" => "https://example.com", "title" => "The Rock", @@ -1770,6 +1771,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do "title" => "Pleroma", "description" => "", "image" => nil, + "image_description" => "", "provider_name" => "example.com", "provider_url" => "https://example.com", "url" => "https://example.com/ogp-missing-data", @@ -1828,6 +1830,60 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do json_response_and_validate_schema(bookmarks, 200) end + test "bookmark folders" do + %{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"]) + + {:ok, folder} = Pleroma.BookmarkFolder.create(user.id, "folder") + author = insert(:user) + + folder_bookmarks_uri = "/api/v1/bookmarks?folder_id=#{folder.id}" + + {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) + {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) + + # Add bookmark with a folder + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/bookmark", %{folder_id: folder.id}) + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == + folder.id + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark") + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == nil + + bookmarks = + get(conn, folder_bookmarks_uri) + |> json_response_and_validate_schema(200) + + assert length(bookmarks) == 1 + + # Update folder for existing bookmark + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark", %{folder_id: folder.id}) + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == + folder.id + + bookmarks = + get(conn, folder_bookmarks_uri) + |> json_response_and_validate_schema(200) + + assert length(bookmarks) == 2 + end + describe "conversation muting" do setup do: oauth_access(["write:mutes"]) diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index cf26cd9a6..bea0cae69 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -511,10 +511,15 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account_data["fields"] == [ - %{"name" => "foo", "value" => "bar"}, + %{ + "name" => "foo", + "value" => "bar", + "verified_at" => nil + }, %{ "name" => "link.io", - "value" => ~S(cofe.io) + "value" => ~S(cofe.io), + "verified_at" => nil } ] @@ -573,8 +578,8 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account_data["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} + %{"name" => ":firefox:", "value" => "is best 2hu", "verified_at" => nil}, + %{"name" => "they wins", "value" => ":blank:", "verified_at" => nil} ] assert account_data["source"]["fields"] == [ @@ -602,10 +607,11 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo", "value" => "bar", "verified_at" => nil}, %{ "name" => "link", - "value" => ~S(http://cofe.io) + "value" => ~S(http://cofe.io), + "verified_at" => nil } ] @@ -627,7 +633,7 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account["fields"] == [ - %{"name" => "foo", "value" => ""} + %{"name" => "foo", "value" => "", "verified_at" => nil} ] end diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index a73d862fd..3aa73c224 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -43,7 +43,8 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do %{title: "why are you even asking?", votes_count: 0} ], votes_count: 0, - voters_count: 0 + voters_count: 0, + pleroma: %{non_anonymous: false} } result = PollView.render("show.json", %{object: object}) @@ -165,4 +166,11 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do ] } = PollView.render("show.json", %{object: object}) end + + test "that poll is non anonymous" do + object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) + result = PollView.render("show.json", %{object: object}) + + assert result[:pleroma][:non_anonymous] == true + end end diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 47da5b048..4a53406a0 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -341,7 +341,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do emoji_reactions: [], parent_visible: false, pinned_at: nil, - quotes_count: 0 + quotes_count: 0, + bookmark_folder: nil } } @@ -772,6 +773,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do page_url = "http://example.com" card = %{ + "image:alt" => "Example image description", url: page_url, site_name: "Example site name", title: "Example website", @@ -779,7 +781,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do description: "Example description" } - %{provider_name: "example.com"} = + %{provider_name: "example.com", image_description: "Example image description"} = StatusView.render("card.json", %{page_url: page_url, rich_media: card}) end diff --git a/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs new file mode 100644 index 000000000..9bd90ed2e --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs @@ -0,0 +1,161 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.BookmarkFolderControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.BookmarkFolder + # alias Pleroma.Object + # alias Pleroma.Tests.Helpers + # alias Pleroma.UnstubbedConfigMock, as: ConfigMock + # alias Pleroma.User + # alias Pleroma.Web.ActivityPub.ActivityPub + # alias Pleroma.Web.CommonAPI + + # import Mox + import Pleroma.Factory + + describe "GET /api/v1/pleroma/bookmark_folders" do + setup do: oauth_access(["read:bookmarks"]) + + test "it lists bookmark folders", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + folder_id = folder.id + + result = + conn + |> get("/api/v1/pleroma/bookmark_folders") + |> json_response_and_validate_schema(200) + + assert [ + %{ + "id" => ^folder_id, + "name" => "Bookmark folder", + "emoji" => nil, + "emoji_url" => nil + } + ] = result + end + end + + describe "POST /api/v1/pleroma/bookmark_folders" do + setup do: oauth_access(["write:bookmarks"]) + + test "it creates a bookmark folder", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: "📁" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "Bookmark folder", + "emoji" => "📁", + "emoji_url" => nil + } = result + end + + test "it creates a bookmark folder with custom emoji", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: ":firefox:" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "Bookmark folder", + "emoji" => ":firefox:", + "emoji_url" => "http://localhost:4001/emoji/Firefox.gif" + } = result + end + + test "it returns error for invalid emoji", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: "not an emoji" + }) + |> json_response_and_validate_schema(422) + + assert %{"error" => "Invalid emoji"} = result + end + end + + describe "PATCH /api/v1/pleroma/bookmark_folders/:id" do + setup do: oauth_access(["write:bookmarks"]) + + test "it updates a bookmark folder", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + result = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{ + name: "bookmark folder" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "bookmark folder" + } = result + end + + test "it returns error when updating others' folders", %{conn: conn} do + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder") + + result = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{ + name: "bookmark folder" + }) + |> json_response_and_validate_schema(403) + + assert %{ + "error" => "Access denied" + } = result + end + end + + describe "DELETE /api/v1/pleroma/bookmark_folders/:id" do + setup do: oauth_access(["write:bookmarks"]) + + test "it deleting a bookmark folder", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + assert conn + |> delete("/api/v1/pleroma/bookmark_folders/#{folder.id}") + |> json_response_and_validate_schema(200) + + folders = BookmarkFolder.for_user(user.id) + + assert length(folders) == 0 + end + + test "it returns error when deleting others' folders", %{conn: conn} do + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder") + + result = + conn + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}") + |> json_response_and_validate_schema(403) + + assert %{ + "error" => "Access denied" + } = result + end + end +end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 771336b6f..0ee33d579 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1503,6 +1503,24 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: "hello"}} end + def get("https://friends.grishka.me/posts/54642", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://friends.grishka.me/users/1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"), + headers: activitypub_object_headers() + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} diff --git a/test/test_helper.exs b/test/test_helper.exs index e65f7c1d1..a117584ae 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -4,6 +4,8 @@ Code.put_compiler_option(:warnings_as_errors, true) +ExUnit.configure(max_cases: System.schedulers_online()) + ExUnit.start(exclude: [:federated, :erratic]) if match?({:unix, :darwin}, :os.type()) do diff --git a/uploads/.gitignore b/uploads/.gitignore deleted file mode 100644 index 523e584a7..000000000 --- a/uploads/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Git will ignore everything in this directory except this file. -* -!.gitignore