diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b4bd59b43..6a2be879e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -63,21 +63,19 @@ unit-testing:
- mix ecto.migrate
- mix coveralls --preload-modules
-# Removed to fix CI issue. In this early state it wasn't adding much value anyway.
-# TODO Fix and reinstate federated testing
-# federated-testing:
-# stage: test
-# cache: *testing_cache_policy
-# services:
-# - name: minibikini/postgres-with-rum:12
-# alias: postgres
-# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
-# script:
-# - mix deps.get
-# - mix ecto.create
-# - mix ecto.migrate
-# - epmd -daemon
-# - mix test --trace --only federated
+federated-testing:
+ stage: test
+ cache: *testing_cache_policy
+ services:
+ - name: minibikini/postgres-with-rum:12
+ alias: postgres
+ command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
+ script:
+ - mix deps.get
+ - mix ecto.create
+ - mix ecto.migrate
+ - epmd -daemon
+ - mix test --trace --only federated
unit-testing-rum:
stage: test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25f79167d..3c005a4dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,10 +16,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
API Changes
+- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed.
+- **Breaking:** Image description length is limited now.
- **Breaking:** Emoji API: changed methods and renamed routes.
+- MastodonAPI: Allow removal of avatar, banner and background.
- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream.
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text.
+- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
@@ -52,6 +56,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
- Support pagination in emoji packs API (for packs and for files in pack)
+- Support for viewing instances favicons next to posts and accounts
API Changes
@@ -59,8 +64,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
-- Mastodon API: Add support for filtering replies in public and home timelines
-- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`
+- Mastodon API: Add support for filtering replies in public and home timelines.
+- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`.
+- Mastodon API: Support irreversible property for filters.
+- Mastodon API: Add pleroma.favicon field to accounts.
- Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs
@@ -74,6 +81,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
+- Rich Media Previews for Twitter links
+- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
+- Fix CSP policy generation to include remote Captcha services
## [Unreleased (patch)]
@@ -215,7 +225,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
-- Mastodon API: Add `pleroma.unread_count` to the Marker entity
- Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
diff --git a/benchmarks/load_testing/activities.ex b/benchmarks/load_testing/activities.ex
index 074ded457..f5c7bfce8 100644
--- a/benchmarks/load_testing/activities.ex
+++ b/benchmarks/load_testing/activities.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do
@visibility ~w(public private direct unlisted)
@types [
:simple,
+ :simple_filtered,
:emoji,
:mentions,
:hell_thread,
@@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status")
end
+ defp insert_activity(:simple_filtered, visibility, group, users, _opts)
+ when group in @remote_groups do
+ insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
+ end
+
+ defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
+ insert_local_activity(visibility, group, users, "Simple status which must be filtered")
+ end
+
defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex
index 15fd06c3d..dfbd916be 100644
--- a/benchmarks/load_testing/fetcher.ex
+++ b/benchmarks/load_testing/fetcher.ex
@@ -32,10 +32,22 @@ defp fetch_user(user) do
)
end
+ defp create_filter(user) do
+ Pleroma.Filter.create(%Pleroma.Filter{
+ user_id: user.id,
+ phrase: "must be filtered",
+ hide: true
+ })
+ end
+
+ defp delete_filter(filter), do: Repo.delete(filter)
+
defp fetch_timelines(user) do
fetch_home_timeline(user)
+ fetch_home_timeline_with_filter(user)
fetch_direct_timeline(user)
fetch_public_timeline(user)
+ fetch_public_timeline_with_filter(user)
fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag)
@@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do
}
end
- defp fetch_home_timeline(user) do
+ defp fetch_home_timeline(user, title_end \\ "") do
opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
@@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do
|> Enum.reverse()
|> List.last()
+ title = "home timeline " <> title_end
+
Benchee.run(
%{
- "home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
+ title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
},
inputs: %{
"1 page" => opts,
@@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do
)
end
+ defp fetch_home_timeline_with_filter(user) do
+ {:ok, filter} = create_filter(user)
+
+ fetch_home_timeline(user, "with filters")
+
+ delete_filter(filter)
+ end
+
defp opts_for_direct_timeline(user) do
%{
visibility: "direct",
@@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do
fetch_public_timeline(opts, "public timeline")
end
+ defp fetch_public_timeline_with_filter(user) do
+ {:ok, filter} = create_filter(user)
+ opts = opts_for_public_timeline(user)
+
+ fetch_public_timeline(opts, "public timeline with filters")
+ delete_filter(filter)
+ end
+
defp fetch_public_timeline(user, :local) do
opts = opts_for_public_timeline(user, :local)
diff --git a/config/config.exs b/config/config.exs
index 9b550920c..3577cd101 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -97,6 +97,7 @@
"dat",
"dweb",
"gopher",
+ "hyper",
"ipfs",
"ipns",
"irc",
@@ -188,6 +189,7 @@
background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000,
+ description_limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
@@ -436,8 +438,7 @@
config :pleroma, Pleroma.Web.Preload,
providers: [
- Pleroma.Web.Preload.Providers.Instance,
- Pleroma.Web.Preload.Providers.StatusNet
+ Pleroma.Web.Preload.Providers.Instance
]
config :pleroma, :http_security,
@@ -705,6 +706,8 @@
config :ex_aws, http_client: Pleroma.HTTP.ExAws
+config :pleroma, :instances_favicons, enabled: false
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index 370af80a6..03b84bfc8 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -498,6 +498,7 @@
"dat",
"dweb",
"gopher",
+ "hyper",
"ipfs",
"ipns",
"irc",
@@ -699,8 +700,9 @@
key: :public,
type: :boolean,
description:
- "Makes the client API in authentificated mode-only except for user-profiles." <>
- " Useful for disabling the Local Timeline and The Whole Known Network."
+ "Makes the client API in authenticated mode-only except for user-profiles." <>
+ " Useful for disabling the Local Timeline and The Whole Known Network. " <>
+ " Note: when setting to `false`, please also check `:restrict_unauthenticated` setting."
},
%{
key: :quarantined_instances,
@@ -3446,5 +3448,18 @@
suggestions: [false]
}
]
+ },
+ %{
+ group: :pleroma,
+ key: :instances_favicons,
+ type: :group,
+ description: "Control favicons for instances",
+ children: [
+ %{
+ key: :enabled,
+ type: :boolean,
+ description: "Allow/disallow displaying and getting instances favicons"
+ }
+ ]
}
]
diff --git a/config/test.exs b/config/test.exs
index e38b9967d..e6596e0bc 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -111,6 +111,8 @@
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+config :pleroma, :instances_favicons, enabled: true
+
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index 4514a7d59..65f9f1aef 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -72,6 +72,7 @@ Has these additional fields under the `pleroma` object:
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
- `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
+- `favicon`: nullable URL string, Favicon image of the user's instance
### Source
@@ -183,11 +184,13 @@ Additional parameters can be added to the JSON body/Form data:
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts
-- `pleroma_background_image` - sets the background image of the user.
+- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
+All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
+
### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
@@ -222,6 +225,8 @@ Has theses additional parameters (which are the same as in Pleroma-API):
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
+- `chat_limit`: The maximum characters per chat message
+- `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars
diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md
index 1e6f4a8b4..3b4c421a7 100644
--- a/docs/administration/CLI_tasks/user.md
+++ b/docs/administration/CLI_tasks/user.md
@@ -57,11 +57,11 @@ mix pleroma.user invites
## Revoke invite
```sh tab="OTP"
- ./bin/pleroma_ctl user revoke_invite
+ ./bin/pleroma_ctl user revoke_invite
```
```sh tab="From Source"
-mix pleroma.user revoke_invite
+mix pleroma.user revoke_invite
```
diff --git a/docs/administration/updating.md b/docs/administration/updating.md
index 2a08dac1f..c994f3f16 100644
--- a/docs/administration/updating.md
+++ b/docs/administration/updating.md
@@ -1,6 +1,6 @@
# Updating your instance
-You should **always check the release notes/changelog** in case there are config deprecations, special update special update steps, etc.
+You should **always check the [release notes/changelog](https://git.pleroma.social/pleroma/pleroma/-/releases)** in case there are config deprecations, special update steps, etc.
Besides that, doing the following is generally enough:
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 6759d5e93..d775534b6 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -18,6 +18,7 @@ To add configuration to your config file, you can copy it from the base config.
* `notify_email`: Email used for notifications.
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
* `limit`: Posts character limit (CW/Subject included in the counter).
+* `discription_limit`: The character limit for image descriptions.
* `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner).
@@ -36,7 +37,7 @@ To add configuration to your config file, you can copy it from the base config.
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
-* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
+* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. See also: `restrict_unauthenticated`.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
@@ -154,7 +155,7 @@ config :pleroma, :mrf_user_allowlist, %{
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely
-#### mrf_steal_emoji
+#### :mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
@@ -970,11 +971,11 @@ config :pleroma, :database_config_whitelist, [
### :restrict_unauthenticated
-Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
+Restrict access for unauthenticated users to timelines (public and federated), user profiles and statuses.
* `timelines`: public and federated timelines
* `local`: public timeline
- * `federated`
+ * `federated`: federated timeline (includes public timeline)
* `profiles`: user profiles
* `local`
* `remote`
@@ -982,7 +983,14 @@ Restrict access for unauthenticated users to timelines (public and federate), us
* `local`
* `remote`
+Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practical sense if `restrict_unauthenticated/timelines/federated` is set to `false` (since local public activities will still be delivered to unauthenticated users as part of federated timeline).
## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
+
+## :instances_favicons
+
+Control favicons for instances.
+
+* `enabled`: Allow/disallow displaying and getting instances favicons
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 3ad6edbfb..9f0bf6ecb 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -3,15 +3,48 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do
+ @apps [
+ :restarter,
+ :ecto,
+ :ecto_sql,
+ :postgrex,
+ :db_connection,
+ :cachex,
+ :flake_id,
+ :swoosh,
+ :timex
+ ]
+ @cachex_children ["object", "user"]
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
+ Pleroma.Config.Holder.save_default()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug)
end
- {:ok, _} = Application.ensure_all_started(:pleroma)
+ apps =
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
+ [:gun | @apps]
+ else
+ [:hackney | @apps]
+ end
+
+ Enum.each(apps, &Application.ensure_all_started/1)
+
+ children = [
+ Pleroma.Repo,
+ {Pleroma.Config.TransferTask, false},
+ Pleroma.Web.Endpoint
+ ]
+
+ cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
+
+ Supervisor.start_link(children ++ cachex_children,
+ strategy: :one_for_one,
+ name: Pleroma.Supervisor
+ )
if Pleroma.Config.get(:env) not in [:test, :benchmark] do
pleroma_rebooted?()
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index 86409738a..91440b453 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -145,7 +145,7 @@ def run(["gen" | rest]) do
options,
:uploads_dir,
"What directory should media uploads go in (when using the local uploader)?",
- Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
+ Config.get([Pleroma.Uploaders.Local, :uploads])
)
|> Path.expand()
@@ -154,7 +154,7 @@ def run(["gen" | rest]) do
options,
:static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
- Pleroma.Config.get([:instance, :static_dir])
+ Config.get([:instance, :static_dir])
)
|> Path.expand()
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index bca7e87bf..01824aa18 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -232,7 +232,7 @@ def run(["tag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
- shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
+ shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
@@ -245,7 +245,7 @@ def run(["untag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
- shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
+ shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 9615af122..84f3aa82d 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -35,7 +35,7 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
- Pleroma.Config.Holder.save_default()
+ Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
@@ -162,7 +162,8 @@ defp idempotency_expiration,
defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
- defp build_cachex(type, opts),
+ @spec build_cachex(String.t(), keyword()) :: map()
+ def build_cachex(type, opts),
do: %{
id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index eb86b8ff4..a0d7b7d71 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, :gopher, [:enabled]}
]
- def start_link(_) do
- load_and_update_env()
+ def start_link(restart_pleroma? \\ true) do
+ load_and_update_env([], restart_pleroma?)
if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore
end
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index 55f61024e..aa0b2a66b 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do
alias Pleroma.Config
alias Pleroma.Web.Router.Helpers
- defp instance_config, do: Pleroma.Config.get(:instance)
+ defp instance_config, do: Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp instance_notify_email do
@@ -72,6 +72,8 @@ def report(to, reporter, account, statuses, comment) do
Reported Account: #{account.nickname}
#{comment_html}
#{statuses_html}
+
+ View Reports in AdminFE
"""
new()
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 3de2dc762..03a6bca0b 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -108,7 +108,7 @@ defp load_pack(pack_dir, emoji_groups) do
if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
- extensions = Pleroma.Config.get([:emoji, :pack_extensions])
+ extensions = Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 4d61b3650..5d6df9530 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do
Repo.one(query)
end
- def get_filters(%User{id: user_id} = _user) do
+ def get_active(query) do
+ from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
+ end
+
+ def get_irreversible(query) do
+ from(f in query, where: f.hide)
+ end
+
+ def get_filters(query \\ __MODULE__, %User{id: user_id}) do
query =
from(
- f in Pleroma.Filter,
+ f in query,
where: f.user_id == ^user_id,
order_by: [desc: :id]
)
@@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do
|> validate_required([:phrase, :context])
|> Repo.update()
end
+
+ def compose_regex(user_or_filters, format \\ :postgres)
+
+ def compose_regex(%User{} = user, format) do
+ __MODULE__
+ |> get_active()
+ |> get_irreversible()
+ |> get_filters(user)
+ |> compose_regex(format)
+ end
+
+ def compose_regex([_ | _] = filters, format) do
+ phrases =
+ filters
+ |> Enum.map(& &1.phrase)
+ |> Enum.join("|")
+
+ case format do
+ :postgres ->
+ "\\y(#{phrases})\\y"
+
+ :re ->
+ ~r/\b#{phrases}\b/i
+
+ _ ->
+ nil
+ end
+ end
+
+ def compose_regex(_, _), do: nil
end
diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex
index 74458c09a..a1f935232 100644
--- a/lib/pleroma/instances/instance.ex
+++ b/lib/pleroma/instances/instance.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do
field(:host, :string)
field(:unreachable_since, :naive_datetime_usec)
+ field(:favicon, :string)
+ field(:favicon_updated_at, :naive_datetime)
timestamps()
end
@@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:host, :unreachable_since])
+ |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|> validate_required([:host])
|> unique_constraint(:host)
end
@@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do
end
defp parse_datetime(datetime), do: datetime
+
+ def get_or_update_favicon(%URI{host: host} = instance_uri) do
+ existing_record = Repo.get_by(Instance, %{host: host})
+ now = NaiveDateTime.utc_now()
+
+ if existing_record && existing_record.favicon_updated_at &&
+ NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
+ existing_record.favicon
+ else
+ favicon = scrape_favicon(instance_uri)
+
+ if existing_record do
+ existing_record
+ |> changeset(%{favicon: favicon, favicon_updated_at: now})
+ |> Repo.update()
+ else
+ %Instance{}
+ |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
+ |> Repo.insert()
+ end
+
+ favicon
+ end
+ end
+
+ defp scrape_favicon(%URI{} = instance_uri) do
+ try do
+ with {:ok, %Tesla.Env{body: html}} <-
+ Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
+ favicon_rel <-
+ html
+ |> Floki.parse_document!()
+ |> Floki.attribute("link[rel=icon]", "href")
+ |> List.first(),
+ favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
+ true <- is_binary(favicon) do
+ favicon
+ else
+ _ -> nil
+ end
+ rescue
+ _ -> nil
+ end
+ end
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 2ef1a80c5..32bcfcaba 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do
|> preload([n, a, o], activity: {a, object: o})
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts)
+ |> exclude_filtered(user)
|> exclude_visibility(opts)
end
@@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do
|> where([n, a, o, tm], is_nil(tm.user_id))
end
+ defp exclude_filtered(query, user) do
+ case Pleroma.Filter.compose_regex(user) do
+ nil ->
+ query
+
+ regex ->
+ from([_n, a, o] in query,
+ where:
+ fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
+ fragment("?->>'actor' = ?", o.data, ^user.ap_id)
+ )
+ end
+ end
+
@valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility})
@@ -337,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do
end
end
+ @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
def create_notifications(activity, options \\ [])
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
@@ -481,6 +497,10 @@ def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_i
end
end
+ def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
+ [object_id]
+ end
+
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
@@ -555,7 +575,8 @@ def skip?(%Activity{} = activity, %User{} = user) do
:follows,
:non_followers,
:non_follows,
- :recently_followed
+ :recently_followed,
+ :filtered
]
|> Enum.find(&skip?(&1, activity, user))
end
@@ -624,6 +645,26 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
end)
end
+ def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
+
+ def skip?(:filtered, activity, user) do
+ object = Object.normalize(activity)
+
+ cond do
+ is_nil(object) ->
+ false
+
+ object.data["actor"] == user.ap_id ->
+ false
+
+ not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
+ Regex.match?(regex, object.data["content"])
+
+ true ->
+ false
+ end
+ end
+
def skip?(_, _, _), do: false
def for_user_and_activity(user, activity) do
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index 1420a9611..7d65cf078 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -69,10 +69,11 @@ defp csp_string do
img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'"
+ # Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src} =
if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
- sources = get_proxy_and_attachment_sources()
+ sources = build_csp_multimedia_source_list()
{[img_src, sources], [media_src, sources]}
else
{[img_src, " https:"], [media_src, " https:"]}
@@ -81,14 +82,14 @@ defp csp_string do
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
connect_src =
- if Pleroma.Config.get(:env) == :dev do
+ if Config.get(:env) == :dev do
[connect_src, " http://localhost:3035/"]
else
connect_src
end
script_src =
- if Pleroma.Config.get(:env) == :dev do
+ if Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"
@@ -107,29 +108,28 @@ defp csp_string do
|> :erlang.iolist_to_binary()
end
- defp get_proxy_and_attachment_sources do
+ defp build_csp_multimedia_source_list do
media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host)
end)
- media_proxy_base_url =
- if Config.get([:media_proxy, :base_url]),
- do: URI.parse(Config.get([:media_proxy, :base_url])).host
+ media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url]))
- upload_base_url =
- if Config.get([Pleroma.Upload, :base_url]),
- do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
+ upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url]))
- s3_endpoint =
- if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
- do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
+ s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint]))
+
+ captcha_method = Config.get([Pleroma.Captcha, :method])
+
+ captcha_endpoint = build_csp_param(Config.get([captcha_method, :endpoint]))
[]
|> add_source(media_proxy_base_url)
|> add_source(upload_base_url)
|> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist)
+ |> add_source(captcha_endpoint)
end
defp add_source(iodata, nil), do: iodata
@@ -139,6 +139,16 @@ defp add_csp_param(csp_iodata, nil), do: csp_iodata
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
+ defp build_csp_param(nil), do: nil
+
+ defp build_csp_param(url) when is_binary(url) do
+ %{host: host, scheme: scheme} = URI.parse(url)
+
+ if scheme do
+ [scheme, "://", host]
+ end
+ end
+
def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do
Logger.warn("
diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex
index 156e6788e..143665c71 100644
--- a/lib/pleroma/plugs/static_fe_plug.ex
+++ b/lib/pleroma/plugs/static_fe_plug.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
def init(options), do: options
def call(conn, _) do
- if enabled?() and accepts_html?(conn) do
+ if enabled?() and requires_html?(conn) do
conn
|> StaticFEController.call(:show)
|> halt()
@@ -20,10 +20,7 @@ def call(conn, _) do
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
- defp accepts_html?(conn) do
- case get_req_header(conn, "accept") do
- [accept | _] -> String.contains?(accept, "text/html")
- _ -> false
- end
+ defp requires_html?(conn) do
+ Phoenix.Controller.get_format(conn) == "html"
end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 797555bff..0fa6b89dc 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -63,6 +63,10 @@ def store(upload, opts \\ []) do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
+ description = Map.get(opts, :description) || upload.name,
+ {_, true} <-
+ {:description_limit,
+ String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
@@ -75,9 +79,12 @@ def store(upload, opts \\ []) do
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
- "name" => Map.get(opts, :description) || upload.name
+ "name" => description
}}
else
+ {:description_limit, _} ->
+ {:error, :description_too_long}
+
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 712bc3047..b9989f901 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -89,7 +89,7 @@ defmodule Pleroma.User do
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
- field(:avatar, :map)
+ field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
@@ -389,8 +389,8 @@ defp fix_follower_address(%{nickname: nickname} = params),
defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
name =
case params[:name] do
@@ -450,8 +450,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
end
def update_changeset(struct, params \\ %{}) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
struct
|> cast(
@@ -542,14 +542,11 @@ defp put_emoji(changeset) do
end
defp put_change_if_present(changeset, map_field, value_function) do
- if value = get_change(changeset, map_field) do
- with {:ok, new_value} <- value_function.(value) do
- put_change(changeset, map_field, new_value)
- else
- _ -> changeset
- end
+ with {:ok, value} <- fetch_change(changeset, map_field),
+ {:ok, new_value} <- value_function.(value) do
+ put_change(changeset, map_field, new_value)
else
- changeset
+ _ -> changeset
end
end
@@ -624,13 +621,13 @@ def force_password_reset_async(user) do
def force_password_reset(user), do: update_password_reset_pending(user, true)
def register_changeset(struct, params \\ %{}, opts \\ []) do
- bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
- name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
+ bio_limit = Config.get([:instance, :user_bio_length], 5000)
+ name_limit = Config.get([:instance, :user_name_length], 100)
params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
- Pleroma.Config.get([:instance, :account_activation_required])
+ Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
@@ -652,7 +649,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
+ |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
@@ -667,7 +664,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
- if Pleroma.Config.get([:instance, :account_activation_required]) do
+ if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
@@ -687,7 +684,7 @@ defp put_following_and_follower_address(changeset) do
end
defp autofollow_users(user) do
- candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
+ candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
@@ -714,7 +711,7 @@ def post_register_action(%User{} = user) do
def try_send_confirmation_email(%User{} = user) do
if user.confirmation_pending &&
- Pleroma.Config.get([:instance, :account_activation_required]) do
+ Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
@@ -771,7 +768,7 @@ def follow_all(follower, followeds) do
defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
- deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+ deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
followed.deactivated ->
@@ -972,7 +969,7 @@ def get_cached_by_nickname(nickname) do
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
- restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
+ restrict_to_local = Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
@@ -1168,7 +1165,7 @@ defp follow_information_changeset(user, params) do
@spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
- if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
+ if user.local or !Config.get([:instance, :external_user_synchronization]) do
follower_count = FollowingRelationship.follower_count(user)
user
@@ -1181,7 +1178,7 @@ def update_follower_count(%User{} = user) do
@spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
- if Pleroma.Config.get([:instance, :external_user_synchronization]) do
+ if Config.get([:instance, :external_user_synchronization]) do
{:ok, maybe_fetch_follow_information(user)}
else
{:ok, user}
@@ -1268,7 +1265,7 @@ def unmute(%User{} = muter, %User{} = mutee) do
end
def subscribe(%User{} = subscriber, %User{} = target) do
- deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+ deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
@@ -1551,7 +1548,7 @@ def perform(:follow_import, %User{} = follower, followed_identifiers)
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
- {:ok, _} <- ActivityPub.follow(follower, followed) do
+ {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed
else
err ->
@@ -1659,7 +1656,7 @@ def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
- def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
+ def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
@@ -1841,7 +1838,7 @@ defp normalize_tags(tags) do
end
defp local_nickname_regex do
- if Pleroma.Config.get([:instance, :extended_nickname_format]) do
+ if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
@@ -1969,8 +1966,8 @@ def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
- config = Pleroma.Config.get([:assets, :mascots])
- default_mascot = Pleroma.Config.get([:assets, :default_mascot])
+ config = Config.get([:assets, :mascots])
+ default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
@@ -2065,7 +2062,7 @@ def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
- limit = Pleroma.Config.get([:instance, limit_name], 0)
+ limit = Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
@@ -2079,8 +2076,8 @@ def validate_fields(changeset, remote? \\ false) do
end
defp valid_field?(%{"name" => name, "value" => value}) do
- name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
- value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
+ name_limit = Config.get([:instance, :account_field_name_length], 255)
+ value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
@@ -2090,10 +2087,10 @@ defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
- String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
+ String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
- String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
+ String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
%{"name" => name, "value" => value}
end
@@ -2148,7 +2145,7 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
- max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+ max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
user
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 42ff1de78..d4fd31069 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -69,11 +69,15 @@ defp fts_search(query, query_string) do
u in query,
where:
fragment(
+ # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
"""
- (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?)
+ (
+ setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
+ ) @@ to_tsquery('simple', ?)
""",
- u.name,
u.nickname,
+ u.name,
^query_string
)
)
@@ -88,15 +92,23 @@ defp to_tsquery(query_string) do
|> Enum.join(" | ")
end
+ # Considers nickname match, localized nickname match, name match; preferences nickname match
defp trigram_rank(query, query_string) do
from(
u in query,
select_merge: %{
search_rank:
fragment(
- "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+ """
+ similarity(?, ?) +
+ similarity(?, regexp_replace(?, '@.+', '')) +
+ similarity(?, trim(coalesce(?, '')))
+ """,
^query_string,
u.nickname,
+ ^query_string,
+ u.nickname,
+ ^query_string,
u.name
)
}
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 17c9d8f21..8da5cf938 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
+ alias Pleroma.Filter
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
@@ -321,28 +322,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
end
end
- @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
- {:ok, Activity.t()} | {:error, any()}
- def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
- with {:ok, result} <-
- Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
- result
- end
- end
-
- defp do_follow(follower, followed, activity_id, local, opts) do
- skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
- data = make_follow_data(follower, followed, activity_id)
-
- with {:ok, activity} <- insert(data, local),
- _ <- skip_notify_and_stream || notify_and_stream(activity),
- :ok <- maybe_federate(activity) do
- {:ok, activity}
- else
- {:error, error} -> Repo.rollback(error)
- end
- end
-
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
@@ -446,6 +425,7 @@ def fetch_activities_for_context_query(context, opts) do
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts[:user])
+ |> restrict_filtered(opts)
|> where(
[activity],
fragment(
@@ -961,6 +941,26 @@ defp restrict_instance(query, %{instance: instance}) do
defp restrict_instance(query, _), do: query
+ defp restrict_filtered(query, %{user: %User{} = user}) do
+ case Filter.compose_regex(user) do
+ nil ->
+ query
+
+ regex ->
+ from([activity, object] in query,
+ where:
+ fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
+ activity.actor == ^user.ap_id
+ )
+ end
+ end
+
+ defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
+ restrict_filtered(query, %{user: user})
+ end
+
+ defp restrict_filtered(query, _), do: query
+
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do
@@ -1091,6 +1091,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts)
+ |> restrict_filtered(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
@@ -1099,6 +1100,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
+ |> restrict_filtered(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
|> exclude_chat_messages(opts)
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index cabc28de9..d5f3610ed 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants
+ @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
+ def follow(follower, followed) do
+ data = %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => follower.ap_id,
+ "type" => "Follow",
+ "object" => followed.ap_id,
+ "to" => [followed.ap_id]
+ }
+
+ {:ok, data, []}
+ end
+
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index b0ccb63c8..a62914135 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -98,7 +98,7 @@ def filter(message), do: {:ok, message}
@impl true
def describe do
mrf_object_age =
- Pleroma.Config.get(:mrf_object_age)
+ Config.get(:mrf_object_age)
|> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}}
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 3092f3272..4fd63106d 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -47,5 +47,5 @@ def filter(object), do: {:ok, object}
@impl true
def describe,
- do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
+ do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 9cea6bcf9..70a2ca053 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -155,7 +155,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
%{host: actor_host} = URI.parse(actor)
reject_deletes =
- Pleroma.Config.get([:mrf_simple, :reject_deletes])
+ Config.get([:mrf_simple, :reject_deletes])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index bb6324460..df926829c 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
@@ -25,6 +26,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
+ 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
diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
new file mode 100644
index 000000000..ca2724616
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
+ use Ecto.Schema
+
+ alias Pleroma.EctoType.ActivityPub.ObjectValidators
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @primary_key false
+
+ embedded_schema do
+ field(:id, ObjectValidators.ObjectID, primary_key: true)
+ field(:type, :string)
+ field(:actor, ObjectValidators.ObjectID)
+ field(:to, ObjectValidators.Recipients, default: [])
+ field(:cc, ObjectValidators.Recipients, default: [])
+ field(:object, ObjectValidators.ObjectID)
+ field(:state, :string, default: "pending")
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> cast(data, __schema__(:fields))
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:id, :type, :actor, :to, :cc, :object])
+ |> validate_inclusion(:type, ["Follow"])
+ |> validate_inclusion(:state, ~w{pending reject accept})
+ |> validate_actor_presence()
+ |> validate_actor_presence(field_name: :object)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data
+ |> validate_data
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index 484178edd..b09764d2b 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -28,7 +28,7 @@ def relay_ap_id do
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
- {:ok, activity} <- ActivityPub.follow(local_user, target_user) do
+ {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 61feeae4d..1d2c296a5 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
+ alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -21,6 +22,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(object, meta \\ [])
+ # Tasks this handle
+ # - Follows if possible
+ # - Sends a notification
+ # - Generates accept or reject if appropriate
+ def handle(
+ %{
+ data: %{
+ "id" => follow_id,
+ "type" => "Follow",
+ "object" => followed_user,
+ "actor" => following_user
+ }
+ } = object,
+ meta
+ ) do
+ with %User{} = follower <- User.get_cached_by_ap_id(following_user),
+ %User{} = followed <- User.get_cached_by_ap_id(followed_user),
+ {_, {:ok, _}, _, _} <-
+ {:following, User.follow(follower, followed, :follow_pending), follower, followed} do
+ if followed.local && !followed.locked do
+ Utils.update_follow_state_for_all(object, "accept")
+ FollowingRelationship.update(follower, followed, :follow_accept)
+ User.update_follower_count(followed)
+ User.update_following_count(follower)
+
+ %{
+ to: [following_user],
+ actor: followed,
+ object: follow_id,
+ local: true
+ }
+ |> ActivityPub.accept()
+ end
+ else
+ {:following, {:error, _}, follower, followed} ->
+ Utils.update_follow_state_for_all(object, "reject")
+ FollowingRelationship.update(follower, followed, :follow_reject)
+
+ if followed.local do
+ %{
+ to: [follower.ap_id],
+ actor: followed,
+ object: follow_id,
+ local: true
+ }
+ |> ActivityPub.reject()
+ end
+
+ _ ->
+ nil
+ end
+
+ {:ok, notifications} = Notification.create_notifications(object, do_send: false)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
+
+ updated_object = Activity.get_by_ap_id(follow_id)
+
+ {:ok, updated_object, meta}
+ end
+
# Tasks this handles:
# - Unfollow and block
def handle(
@@ -209,14 +273,20 @@ def handle_object_creation(object) do
{:ok, object}
end
- def handle_undoing(%{data: %{"type" => "Like"}} = object) do
- with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
- {:ok, _} <- Utils.remove_like_from_object(object, liked_object),
- {:ok, _} <- Repo.delete(object) do
- :ok
+ defp undo_like(nil, object), do: delete_object(object)
+
+ defp undo_like(%Object{} = liked_object, object) do
+ with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
+ delete_object(object)
end
end
+ def handle_undoing(%{data: %{"type" => "Like"}} = object) do
+ object.data["object"]
+ |> Object.get_by_ap_id()
+ |> undo_like(object)
+ end
+
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
@@ -246,6 +316,11 @@ def handle_undoing(
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
+ @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
+ defp delete_object(object) do
+ with {:ok, _} <- Repo.delete(object), do: :ok
+ end
+
defp send_notifications(meta) do
Keyword.get(meta, :notifications, [])
|> Enum.each(fn notification ->
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index bc6fc4bd8..884646ceb 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -233,18 +233,24 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
+ true -> nil
end
- attachment_url =
- %{"href" => href}
- |> Maps.put_if_present("mediaType", media_type)
- |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
+ if href do
+ attachment_url =
+ %{"href" => href}
+ |> Maps.put_if_present("mediaType", media_type)
+ |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
- %{"url" => [attachment_url]}
- |> Maps.put_if_present("mediaType", media_type)
- |> Maps.put_if_present("type", data["type"])
- |> Maps.put_if_present("name", data["name"])
+ %{"url" => [attachment_url]}
+ |> Maps.put_if_present("mediaType", media_type)
+ |> Maps.put_if_present("type", data["type"])
+ |> Maps.put_if_present("name", data["name"])
+ else
+ nil
+ end
end)
+ |> Enum.filter(& &1)
Map.put(object, "attachment", attachments)
end
@@ -263,12 +269,18 @@ def fix_url(%{"url" => url} = object) when is_map(url) do
def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type in ["Video", "Audio"] and is_list(url) do
- first_element = Enum.at(url, 0)
+ attachment =
+ Enum.find(url, fn x ->
+ media_type = x["mediaType"] || x["mimeType"] || ""
- link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
+ is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
+ end)
+
+ link_element =
+ Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
object
- |> Map.put("attachment", [first_element])
+ |> Map.put("attachment", [attachment])
|> Map.put("url", link_element["href"])
end
@@ -517,66 +529,6 @@ def handle_incoming(
end
end
- def handle_incoming(
- %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
- _options
- ) do
- with %User{local: true} = followed <-
- User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
- {:ok, %User{} = follower} <-
- User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
- {:ok, activity} <-
- ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
- with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
- {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
- {_, false} <- {:user_locked, User.locked?(followed)},
- {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
- {_, {:ok, _}} <-
- {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
- {:ok, _relationship} <-
- FollowingRelationship.update(follower, followed, :follow_accept) do
- ActivityPub.accept(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
- else
- {:user_blocked, true} ->
- {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
-
- ActivityPub.reject(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
-
- {:follow, {:error, _}} ->
- {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
-
- ActivityPub.reject(%{
- to: [follower.ap_id],
- actor: followed,
- object: data,
- local: true
- })
-
- {:user_locked, true} ->
- {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
- :noop
- end
-
- ActivityPub.notify_and_stream(activity)
- {:ok, activity}
- else
- _e ->
- :error
- end
- end
-
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options
@@ -684,7 +636,7 @@ def handle_incoming(
%{"type" => type} = data,
_options
)
- when type in ~w{Update Block} do
+ when type in ~w{Update Block Follow} do
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index f9545d895..e5f14269a 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -206,8 +206,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
end
end
- def user_show(conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+ def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
@@ -233,11 +233,11 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
|> render("index.json", %{activities: activities, as: :activity})
end
- def list_user_statuses(conn, %{"nickname" => nickname} = params) do
+ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == true
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
{_, page_size} = page_params(params)
activities =
@@ -526,7 +526,7 @@ def disable_mfa(conn, %{"nickname" => nickname}) do
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin})
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index 3c05fa55f..952d9347b 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -203,14 +203,23 @@ def follow_operation do
security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Follow the given account",
parameters: [
- %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
- Operation.parameter(
- :reblogs,
- :query,
- BooleanLike,
- "Receive this account's reblogs in home timeline? Defaults to true."
- )
+ %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
],
+ requestBody:
+ request_body(
+ "Parameters",
+ %Schema{
+ type: :object,
+ properties: %{
+ reblogs: %Schema{
+ type: :boolean,
+ description: "Receive this account's reblogs in home timeline? Defaults to true.",
+ default: true
+ }
+ }
+ },
+ required: false
+ ),
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
@@ -438,6 +447,7 @@ defp create_request do
}
end
+ # TODO: This is actually a token respone, but there's no oauth operation file yet.
defp create_response do
%Schema{
title: "AccountCreateResponse",
@@ -446,14 +456,20 @@ defp create_response do
properties: %{
token_type: %Schema{type: :string},
access_token: %Schema{type: :string},
- scope: %Schema{type: :array, items: %Schema{type: :string}},
- created_at: %Schema{type: :integer, format: :"date-time"}
+ refresh_token: %Schema{type: :string},
+ scope: %Schema{type: :string},
+ created_at: %Schema{type: :integer, format: :"date-time"},
+ me: %Schema{type: :string},
+ expires_in: %Schema{type: :integer}
},
example: %{
+ "token_type" => "Bearer",
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
+ "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
"created_at" => 1_585_918_714,
- "scope" => ["read", "write", "follow", "push"],
- "token_type" => "Bearer"
+ "expires_in" => 600,
+ "scope" => "read write follow push",
+ "me" => "https://gensokyo.2hu/users/raymoo"
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
index 90922c064..97836b2eb 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -4,7 +4,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation
- alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
@@ -40,48 +39,6 @@ def confirmation_resend_operation do
}
end
- def update_avatar_operation do
- %Operation{
- tags: ["Accounts"],
- summary: "Set/clear user avatar image",
- operationId: "PleromaAPI.AccountController.update_avatar",
- requestBody:
- request_body("Parameters", update_avatar_or_background_request(), required: true),
- security: [%{"oAuth" => ["write:accounts"]}],
- responses: %{
- 200 => update_response(),
- 403 => Operation.response("Forbidden", "application/json", ApiError)
- }
- }
- end
-
- def update_banner_operation do
- %Operation{
- tags: ["Accounts"],
- summary: "Set/clear user banner image",
- operationId: "PleromaAPI.AccountController.update_banner",
- requestBody: request_body("Parameters", update_banner_request(), required: true),
- security: [%{"oAuth" => ["write:accounts"]}],
- responses: %{
- 200 => update_response()
- }
- }
- end
-
- def update_background_operation do
- %Operation{
- tags: ["Accounts"],
- summary: "Set/clear user background image",
- operationId: "PleromaAPI.AccountController.update_background",
- security: [%{"oAuth" => ["write:accounts"]}],
- requestBody:
- request_body("Parameters", update_avatar_or_background_request(), required: true),
- responses: %{
- 200 => update_response()
- }
- }
- end
-
def favourites_operation do
%Operation{
tags: ["Accounts"],
@@ -136,52 +93,4 @@ defp id_param do
required: true
)
end
-
- defp update_avatar_or_background_request do
- %Schema{
- title: "PleromaAccountUpdateAvatarOrBackgroundRequest",
- type: :object,
- properties: %{
- img: %Schema{
- nullable: true,
- type: :string,
- format: :binary,
- description: "Image encoded using `multipart/form-data` or an empty string to clear"
- }
- }
- }
- end
-
- defp update_banner_request do
- %Schema{
- title: "PleromaAccountUpdateBannerRequest",
- type: :object,
- properties: %{
- banner: %Schema{
- type: :string,
- nullable: true,
- format: :binary,
- description: "Image encoded using `multipart/form-data` or an empty string to clear"
- }
- }
- }
- end
-
- defp update_response do
- Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{
- type: :object,
- properties: %{
- url: %Schema{
- type: :string,
- format: :uri,
- nullable: true,
- description: "Image URL"
- }
- },
- example: %{
- "url" =>
- "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif"
- }
- })
- end
end
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index 3a84a1593..cf148bc9d 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -103,7 +103,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description:
"A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"
},
- accepts_chat_messages: %Schema{type: :boolean, nullable: true}
+ accepts_chat_messages: %Schema{type: :boolean, nullable: true},
+ favicon: %Schema{
+ type: :string,
+ format: :uri,
+ nullable: true,
+ description: "Favicon image of the user's instance"
+ }
}
},
source: %Schema{
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index fd7149079..4d5b0decf 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -101,10 +101,14 @@ def unblock(blocker, blocked) do
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
- with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
- {:ok, activity} <- ActivityPub.follow(follower, followed),
+ with {:ok, follow_data, _} <- Builder.follow(follower, followed),
+ {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
- {:ok, follower, followed, activity}
+ if activity.data["state"] == "reject" do
+ {:error, :rejected}
+ else
+ {:ok, follower, followed, activity}
+ end
end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 15594125f..9c38b73eb 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -143,7 +143,7 @@ def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do
- limits = Pleroma.Config.get([:instance, :poll_limits])
+ limits = Config.get([:instance, :poll_limits])
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
@@ -502,7 +502,7 @@ def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
- max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
+ max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
@@ -564,7 +564,7 @@ def validate_character_limit("" = _full_payload, [] = _attachments) do
end
def validate_character_limit(full_payload, _attachments) do
- limit = Pleroma.Config.get([:instance, :limit])
+ limit = Config.get([:instance, :limit])
length = String.length(full_payload)
if length <= limit do
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 7ff767db6..fe5d022f5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
@@ -101,12 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
- json(conn, %{
- token_type: "Bearer",
- access_token: token.token,
- scope: app.scopes,
- created_at: Token.Utils.format_created_at(token)
- })
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
{:error, error} -> json_response(conn, :bad_request, %{error: error})
end
@@ -148,6 +144,13 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{})
+ # We use an empty string as a special value to reset
+ # avatars, banners, backgrounds
+ user_image_value = fn
+ "" -> {:ok, nil}
+ value -> {:ok, value}
+ end
+
user_params =
[
:no_rich_text,
@@ -169,9 +172,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note])
- |> Maps.put_if_present(:avatar, params[:avatar])
- |> Maps.put_if_present(:banner, params[:header])
- |> Maps.put_if_present(:background, params[:pleroma_background_image])
+ |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
+ |> Maps.put_if_present(:banner, params[:header], user_image_value)
+ |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present(
:raw_fields,
params[:fields_attributes],
@@ -347,7 +350,7 @@ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"}
end
- def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do
+ def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 3f4c53437..12be530c9 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -201,15 +201,13 @@ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
@doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- render <-
- try_render(conn, "show.json",
- activity: activity,
- for: user,
- with_direct_conversation_id: true,
- with_source: true
- ),
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- render
+ try_render(conn, "show.json",
+ activity: activity,
+ for: user,
+ with_direct_conversation_id: true,
+ with_source: true
+ )
else
_e -> {:error, :not_found}
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
index 4bdd46d7e..ab7b1d6aa 100644
--- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -88,21 +88,20 @@ def direct(%{assigns: %{user: user}} = conn, params) do
)
end
+ defp restrict_unauthenticated?(true = _local_only) do
+ Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local])
+ end
+
+ defp restrict_unauthenticated?(_) do
+ Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated])
+ end
+
# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local]
- cfg_key =
- if local_only do
- :local
- else
- :federated
- end
-
- restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
-
- if restrict? and is_nil(user) do
- render_error(conn, :unauthorized, "authorization required for timeline view")
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
else
activities =
params
@@ -123,6 +122,10 @@ def public(%{assigns: %{user: user}} = conn, params) do
end
end
+ defp fail_on_bad_auth(conn) do
+ render_error(conn, :unauthorized, "authorization required for timeline view")
+ end
+
defp hashtag_fetching(params, user, local_only) do
tags =
[params[:tag], params[:any]]
@@ -157,15 +160,20 @@ defp hashtag_fetching(params, user, local_only) do
# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local]
- activities = hashtag_fetching(params, user, local_only)
- conn
- |> add_link_headers(activities, %{"local" => local_only})
- |> render("index.json",
- activities: activities,
- for: user,
- as: :activity
- )
+ if is_nil(user) and restrict_unauthenticated?(local_only) do
+ fail_on_bad_auth(conn)
+ else
+ activities = hashtag_fetching(params, user, local_only)
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json",
+ activities: activities,
+ for: user,
+ as: :activity
+ )
+ end
end
# GET /api/v1/timelines/list/:list_id
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 6a643bfcc..bc9745044 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -204,6 +204,18 @@ defp do_render("show.json", %{user: user} = opts) do
%{}
end
+ favicon =
+ if Pleroma.Config.get([:instances_favicons, :enabled]) do
+ user
+ |> Map.get(:ap_id, "")
+ |> URI.parse()
+ |> URI.merge("/")
+ |> Pleroma.Instances.Instance.get_or_update_favicon()
+ |> MediaProxy.url()
+ else
+ nil
+ end
+
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@@ -246,7 +258,8 @@ defp do_render("show.json", %{user: user} = opts) do
relationship: relationship,
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
- accepts_chat_messages: user.accepts_chat_messages
+ accepts_chat_messages: user.accepts_chat_messages,
+ favicon: favicon
}
}
|> maybe_put_role(user, opts[:for])
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 89e48fba5..5deb0d7ed 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -34,6 +34,8 @@ def render("show.json", _) do
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
background_image: Keyword.get(instance, :background_image),
+ chat_limit: Keyword.get(instance, :chat_limit),
+ description_limit: Keyword.get(instance, :description_limit),
pleroma: %{
metadata: %{
account_activation_required: Keyword.get(instance, :account_activation_required),
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index 077fabe47..6f35826da 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -106,7 +106,7 @@ def filename(url_or_path) do
def build_url(sig_base64, url_base64, filename \\ nil) do
[
- Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
+ Config.get([:media_proxy, :base_url], Web.base_url()),
"proxy",
sig_base64,
url_base64,
diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex
index 53e19f82e..f102c93e7 100644
--- a/lib/pleroma/web/oauth/mfa_controller.ex
+++ b/lib/pleroma/web/oauth/mfa_controller.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify])
@@ -74,7 +75,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, Token.Response.build(user, token))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
_error ->
conn
diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex
index 41d5578dc..5d87db268 100644
--- a/lib/pleroma/web/oauth/mfa_view.ex
+++ b/lib/pleroma/web/oauth/mfa_view.ex
@@ -5,4 +5,13 @@
defmodule Pleroma.Web.OAuth.MFAView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+ alias Pleroma.MFA
+
+ def render("mfa_response.json", %{token: token, user: user}) do
+ %{
+ error: "mfa_required",
+ mfa_token: token.token,
+ supported_challenge_types: MFA.supported_methods(user)
+ }
+ end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index c557778ca..7683589cf 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.MFAController
+ alias Pleroma.Web.OAuth.MFAView
+ alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@@ -233,9 +235,7 @@ def token_exchange(
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
- response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
- json(conn, Token.Response.build(user, token, response_attrs))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
_error -> render_invalid_credentials_error(conn)
end
@@ -247,9 +247,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
- response_attrs = %{created_at: Token.Utils.format_created_at(token)}
-
- json(conn, Token.Response.build(user, token, response_attrs))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
error ->
handle_token_exchange_error(conn, error)
@@ -267,7 +265,7 @@ def token_exchange(
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, Token.Response.build(user, token))
+ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
error ->
handle_token_exchange_error(conn, error)
@@ -290,7 +288,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, Token.Response.build_for_client_credentials(token))
+ json(conn, OAuthView.render("token.json", %{token: token}))
else
_error ->
handle_token_exchange_error(conn, :invalid_credentails)
@@ -548,7 +546,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create_token(user, auth) do
- Token.Response.build_for_mfa_token(user, token)
+ MFAView.render("mfa_response.json", %{token: token, user: user})
end
end
diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex
index 94ddaf913..f55247ebd 100644
--- a/lib/pleroma/web/oauth/oauth_view.ex
+++ b/lib/pleroma/web/oauth/oauth_view.ex
@@ -5,4 +5,26 @@
defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
+
+ alias Pleroma.Web.OAuth.Token.Utils
+
+ def render("token.json", %{token: token} = opts) do
+ response = %{
+ token_type: "Bearer",
+ access_token: token.token,
+ refresh_token: token.refresh_token,
+ expires_in: expires_in(),
+ scope: Enum.join(token.scopes, " "),
+ created_at: Utils.format_created_at(token)
+ }
+
+ if user = opts[:user] do
+ response
+ |> Map.put(:me, user.ap_id)
+ else
+ response
+ end
+ end
+
+ defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
deleted file mode 100644
index 0e72c31e9..000000000
--- a/lib/pleroma/web/oauth/token/response.ex
+++ /dev/null
@@ -1,45 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OAuth.Token.Response do
- @moduledoc false
-
- alias Pleroma.MFA
- alias Pleroma.User
- alias Pleroma.Web.OAuth.Token.Utils
-
- @doc false
- def build(%User{} = user, token, opts \\ %{}) do
- %{
- token_type: "Bearer",
- access_token: token.token,
- refresh_token: token.refresh_token,
- expires_in: expires_in(),
- scope: Enum.join(token.scopes, " "),
- me: user.ap_id
- }
- |> Map.merge(opts)
- end
-
- def build_for_client_credentials(token) do
- %{
- token_type: "Bearer",
- access_token: token.token,
- refresh_token: token.refresh_token,
- created_at: Utils.format_created_at(token),
- expires_in: expires_in(),
- scope: Enum.join(token.scopes, " ")
- }
- end
-
- def build_for_mfa_token(user, mfa_token) do
- %{
- error: "mfa_required",
- mfa_token: mfa_token.token,
- supported_challenge_types: MFA.supported_methods(user)
- }
- end
-
- defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
-end
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index f3554d919..563edded7 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
- alias Ecto.Changeset
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
@@ -35,17 +34,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
)
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:accounts"]}
- # Note: the following actions are not permission-secured in Mastodon:
- when action in [
- :update_avatar,
- :update_banner,
- :update_background
- ]
- )
-
plug(
OAuthScopesPlug,
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
@@ -68,56 +56,6 @@ def confirmation_resend(conn, params) do
end
end
- @doc "PATCH /api/v1/pleroma/accounts/update_avatar"
- def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
- {:ok, _user} =
- user
- |> Changeset.change(%{avatar: nil})
- |> User.update_and_set_cache()
-
- json(conn, %{url: nil})
- end
-
- def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do
- {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
- {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
- %{"url" => [%{"href" => href} | _]} = data
-
- json(conn, %{url: href})
- end
-
- @doc "PATCH /api/v1/pleroma/accounts/update_banner"
- def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do
- with {:ok, _user} <- User.update_banner(user, %{}) do
- json(conn, %{url: nil})
- end
- end
-
- def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do
- with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner),
- {:ok, _user} <- User.update_banner(user, object.data) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- @doc "PATCH /api/v1/pleroma/accounts/update_background"
- def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
- with {:ok, _user} <- User.update_background(user, %{}) do
- json(conn, %{url: nil})
- end
- end
-
- def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do
- with {:ok, object} <- ActivityPub.upload(params, type: :background),
- {:ok, _user} <- User.update_background(user, object.data) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
@doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites")
diff --git a/lib/pleroma/web/preload/status_net.ex b/lib/pleroma/web/preload/status_net.ex
deleted file mode 100644
index 9b62f87a2..000000000
--- a/lib/pleroma/web/preload/status_net.ex
+++ /dev/null
@@ -1,25 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Preload.Providers.StatusNet do
- alias Pleroma.Web.Preload.Providers.Provider
- alias Pleroma.Web.TwitterAPI.UtilController
-
- @behaviour Provider
- @config_url "/api/statusnet/config.json"
-
- @impl Provider
- def generate_terms(_params) do
- %{}
- |> build_config_tag()
- end
-
- defp build_config_tag(acc) do
- resp =
- Plug.Test.conn(:get, @config_url |> to_string())
- |> UtilController.config(nil)
-
- Map.put(acc, @config_url, resp.resp_body)
- end
-end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
index ef5ead2da..c8a767935 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -86,7 +86,10 @@ defp parse_url(url) do
end
try do
- {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts)
+ rich_media_agent = Pleroma.Application.user_agent() <> "; Bot"
+
+ {:ok, %Tesla.Env{body: html}} =
+ Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts)
html
|> parse_html()
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9e457848e..386308362 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
- patch("/accounts/update_avatar", AccountController, :update_avatar)
- patch("/accounts/update_banner", AccountController, :update_banner)
- patch("/accounts/update_background", AccountController, :update_background)
-
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
@@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do
pipe_through(:config)
- get("/help/test", TwitterAPI.UtilController, :help_test)
- post("/help/test", TwitterAPI.UtilController, :help_test)
- get("/statusnet/config", TwitterAPI.UtilController, :config)
- get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index aaca182ec..f02c4075c 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -13,9 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- alias Pleroma.Web
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.TwitterAPI.UtilView
alias Pleroma.Web.WebFinger
plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
@@ -42,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
- plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
-
- def help_test(conn, _params) do
- json(conn, "ok")
- end
-
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
@@ -89,80 +81,14 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_
end
end
- def config(%{assigns: %{format: "xml"}} = conn, _params) do
- instance = Pleroma.Config.get(:instance)
- response = UtilView.status_net_config(instance)
-
- conn
- |> put_resp_content_type("application/xml")
- |> send_resp(200, response)
- end
-
- def config(conn, _params) do
- instance = Pleroma.Config.get(:instance)
-
- vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
-
- uploadlimit = %{
- uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
- avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
- backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
- bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
- }
-
- data = %{
- name: Keyword.get(instance, :name),
- description: Keyword.get(instance, :description),
- server: Web.base_url(),
- textlimit: to_string(Keyword.get(instance, :limit)),
- uploadlimit: uploadlimit,
- closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
- private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
- vapidPublicKey: vapid_public_key,
- accountActivationRequired:
- bool_to_val(Keyword.get(instance, :account_activation_required, false)),
- invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
- safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
- }
-
- managed_config = Keyword.get(instance, :managed_config)
-
- data =
- if managed_config do
- pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
- Map.put(data, "pleromafe", pleroma_fe)
- else
- data
- end
-
- json(conn, %{site: data})
- end
-
- defp bool_to_val(true), do: "1"
- defp bool_to_val(_), do: "0"
- defp bool_to_val(true, val, _), do: val
- defp bool_to_val(_, _, val), do: val
-
def frontend_configurations(conn, _params) do
config =
- Pleroma.Config.get(:frontend_configurations, %{})
+ Config.get(:frontend_configurations, %{})
|> Enum.into(%{})
json(conn, config)
end
- def version(%{assigns: %{format: "xml"}} = conn, _params) do
- version = Pleroma.Application.named_version()
-
- conn
- |> put_resp_content_type("application/xml")
- |> send_resp(200, "#{version} ")
- end
-
- def version(conn, _params) do
- json(conn, Pleroma.Application.named_version())
- end
-
def emoji(conn, _params) do
emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
diff --git a/mix.exs b/mix.exs
index e2ab53bde..126aa4709 100644
--- a/mix.exs
+++ b/mix.exs
@@ -178,6 +178,7 @@ defp deps do
ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
+ {:prometheus, "~> 4.6"},
{:prometheus_ex, "~> 3.0"},
{:prometheus_plugs, "~> 1.1"},
{:prometheus_phoenix, "~> 1.3"},
diff --git a/mix.lock b/mix.lock
index 4f2777fa7..30464ddf2 100644
--- a/mix.lock
+++ b/mix.lock
@@ -92,7 +92,7 @@
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4737ce62a31747b4c63c12b20c62307e51bb4fcd730ca0c32c280991e0606c90"},
"pot": {:hex, :pot, "0.10.2", "9895c83bcff8cd22d9f5bc79dfc88a188176b261b618ad70d93faf5c5ca36e67", [:rebar3], [], "hexpm", "ac589a8e296b7802681e93cd0a436faec117ea63e9916709c628df31e17e91e2"},
- "prometheus": {:hex, :prometheus, "4.5.0", "8f4a2246fe0beb50af0f77c5e0a5bb78fe575c34a9655d7f8bc743aad1c6bf76", [:mix, :rebar3], [], "hexpm", "679b5215480fff612b8351f45c839d995a07ce403e42ff02f1c6b20960d41a4e"},
+ "prometheus": {:hex, :prometheus, "4.6.0", "20510f381db1ccab818b4cf2fac5fa6ab5cc91bc364a154399901c001465f46f", [:mix, :rebar3], [], "hexpm", "4905fd2992f8038eccd7aa0cd22f40637ed618c0bed1f75c05aacec15b7545de"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"},
"prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"},
diff --git a/priv/gettext/it/LC_MESSAGES/errors.po b/priv/gettext/it/LC_MESSAGES/errors.po
index 726be628b..406a297d1 100644
--- a/priv/gettext/it/LC_MESSAGES/errors.po
+++ b/priv/gettext/it/LC_MESSAGES/errors.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-06-19 14:33+0000\n"
-"PO-Revision-Date: 2020-06-19 20:38+0000\n"
+"PO-Revision-Date: 2020-07-09 14:40+0000\n"
"Last-Translator: Ben Is \n"
"Language-Team: Italian \n"
@@ -29,258 +29,258 @@ msgstr "non può essere nullo"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
-msgstr ""
+msgstr "è stato già creato"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
-msgstr ""
+msgstr "non è valido"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
-msgstr ""
+msgstr "è in un formato invalido"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
-msgstr ""
+msgstr "ha una voce invalida"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
-msgstr ""
+msgstr "è vietato"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
-msgstr ""
+msgstr "non corrisponde alla verifica"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
-msgstr ""
+msgstr "è ancora associato con questa voce"
msgid "are still associated with this entry"
-msgstr ""
+msgstr "sono ancora associati con questa voce"
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe essere %{count} carattere"
+msgstr[1] "dovrebbero essere %{count} caratteri"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe avere %{count} voce"
+msgstr[1] "dovrebbe avere %{count} voci"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe contenere almeno %{count} carattere"
+msgstr[1] "dovrebbe contenere almeno %{count} caratteri"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe avere almeno %{count} voce"
+msgstr[1] "dovrebbe avere almeno %{count} voci"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe avere al massimo %{count} carattere"
+msgstr[1] "dovrebbe avere al massimo %{count} caratteri"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "dovrebbe avere al massimo %{count} voce"
+msgstr[1] "dovrebbe avere al massimo %{count} voci"
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
-msgstr ""
+msgstr "dev'essere minore di %{number}"
msgid "must be greater than %{number}"
-msgstr ""
+msgstr "dev'essere maggiore di %{number}"
msgid "must be less than or equal to %{number}"
-msgstr ""
+msgstr "dev'essere minore o uguale a %{number}"
msgid "must be greater than or equal to %{number}"
-msgstr ""
+msgstr "dev'essere maggiore o uguale a %{number}"
msgid "must be equal to %{number}"
-msgstr ""
+msgstr "dev'essere uguale a %{number}"
#: lib/pleroma/web/common_api/common_api.ex:421
#, elixir-format
msgid "Account not found"
-msgstr ""
+msgstr "Profilo non trovato"
#: lib/pleroma/web/common_api/common_api.ex:249
#, elixir-format
msgid "Already voted"
-msgstr ""
+msgstr "Hai già votato"
#: lib/pleroma/web/oauth/oauth_controller.ex:360
#, elixir-format
msgid "Bad request"
-msgstr ""
+msgstr "Richiesta invalida"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425
#, elixir-format
msgid "Can't delete object"
-msgstr ""
+msgstr "Non puoi eliminare quest'oggetto"
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196
#, elixir-format
msgid "Can't delete this post"
-msgstr ""
+msgstr "Non puoi eliminare questo messaggio"
#: lib/pleroma/web/controller_helper.ex:95
#: lib/pleroma/web/controller_helper.ex:101
#, elixir-format
msgid "Can't display this activity"
-msgstr ""
+msgstr "Non puoi vedere questo elemento"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:227
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
#, elixir-format
msgid "Can't find user"
-msgstr ""
+msgstr "Non trovo questo utente"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114
#, elixir-format
msgid "Can't get favorites"
-msgstr ""
+msgstr "Non posso ricevere i gradimenti"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437
#, elixir-format
msgid "Can't like object"
-msgstr ""
+msgstr "Non posso gradire quest'oggetto"
#: lib/pleroma/web/common_api/utils.ex:556
#, elixir-format
msgid "Cannot post an empty status without attachments"
-msgstr ""
+msgstr "Non puoi pubblicare un messaggio vuoto senza allegati"
#: lib/pleroma/web/common_api/utils.ex:504
#, elixir-format
msgid "Comment must be up to %{max_size} characters"
-msgstr ""
+msgstr "I commenti posso al massimo consistere di %{max_size} caratteri"
#: lib/pleroma/config/config_db.ex:222
#, elixir-format
msgid "Config with params %{params} not found"
-msgstr ""
+msgstr "Configurazione con parametri %{max_size} non trovata"
#: lib/pleroma/web/common_api/common_api.ex:95
#, elixir-format
msgid "Could not delete"
-msgstr ""
+msgstr "Non eliminato"
#: lib/pleroma/web/common_api/common_api.ex:141
#, elixir-format
msgid "Could not favorite"
-msgstr ""
+msgstr "Non gradito"
#: lib/pleroma/web/common_api/common_api.ex:370
#, elixir-format
msgid "Could not pin"
-msgstr ""
+msgstr "Non intestato"
#: lib/pleroma/web/common_api/common_api.ex:112
#, elixir-format
msgid "Could not repeat"
-msgstr ""
+msgstr "Non ripetuto"
#: lib/pleroma/web/common_api/common_api.ex:188
#, elixir-format
msgid "Could not unfavorite"
-msgstr ""
+msgstr "Non sgradito"
#: lib/pleroma/web/common_api/common_api.ex:380
#, elixir-format
msgid "Could not unpin"
-msgstr ""
+msgstr "Non de-intestato"
#: lib/pleroma/web/common_api/common_api.ex:126
#, elixir-format
msgid "Could not unrepeat"
-msgstr ""
+msgstr "Non de-ripetuto"
#: lib/pleroma/web/common_api/common_api.ex:428
#: lib/pleroma/web/common_api/common_api.ex:437
#, elixir-format
msgid "Could not update state"
-msgstr ""
+msgstr "Non aggiornato"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
#, elixir-format
msgid "Error."
-msgstr ""
+msgstr "Errore."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format
msgid "Invalid CAPTCHA"
-msgstr ""
+msgstr "CAPTCHA invalido"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117
#: lib/pleroma/web/oauth/oauth_controller.ex:569
#, elixir-format
msgid "Invalid credentials"
-msgstr ""
+msgstr "Credenziali invalide"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format
msgid "Invalid credentials."
-msgstr ""
+msgstr "Credenziali invalide."
#: lib/pleroma/web/common_api/common_api.ex:265
#, elixir-format
msgid "Invalid indices"
-msgstr ""
+msgstr "Indici invalidi"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147
#, elixir-format
msgid "Invalid parameters"
-msgstr ""
+msgstr "Parametri invalidi"
#: lib/pleroma/web/common_api/utils.ex:411
#, elixir-format
msgid "Invalid password."
-msgstr ""
+msgstr "Parola d'ordine invalida."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187
#, elixir-format
msgid "Invalid request"
-msgstr ""
+msgstr "Richiesta invalida"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
-msgstr ""
+msgstr "Servizio Kocaptcha non disponibile"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113
#, elixir-format
msgid "Missing parameters"
-msgstr ""
+msgstr "Parametri mancanti"
#: lib/pleroma/web/common_api/utils.ex:540
#, elixir-format
msgid "No such conversation"
-msgstr ""
+msgstr "Conversazione inesistente"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:439
#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507
#, elixir-format
msgid "No such permission_group"
-msgstr ""
+msgstr "permission_group non esistente"
#: lib/pleroma/plugs/uploaded_media.ex:74
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135
#: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format
msgid "Not found"
-msgstr ""
+msgstr "Non trovato"
#: lib/pleroma/web/common_api/common_api.ex:241
#, elixir-format
msgid "Poll's author can't vote"
-msgstr ""
+msgstr "L'autore del sondaggio non può votare"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
@@ -288,215 +288,215 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format
msgid "Record not found"
-msgstr ""
+msgstr "Voce non trovata"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153
#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#, elixir-format
msgid "Something went wrong"
-msgstr ""
+msgstr "C'è stato un problema"
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
-msgstr ""
+msgstr "Il messaggio dev'essere privato"
#: lib/pleroma/web/common_api/utils.ex:566
#, elixir-format
msgid "The status is over the character limit"
-msgstr ""
+msgstr "Il messaggio ha superato la lunghezza massima"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
-msgstr ""
+msgstr "Accedi per leggere."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
msgid "Throttled"
-msgstr ""
+msgstr "Strozzato"
#: lib/pleroma/web/common_api/common_api.ex:266
#, elixir-format
msgid "Too many choices"
-msgstr ""
+msgstr "Troppe alternative"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442
#, elixir-format
msgid "Unhandled activity type"
-msgstr ""
+msgstr "Tipo di attività non gestibile"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:536
#, elixir-format
msgid "You can't revoke your own admin status."
-msgstr ""
+msgstr "Non puoi divestirti da solo."
#: lib/pleroma/web/oauth/oauth_controller.ex:218
#: lib/pleroma/web/oauth/oauth_controller.ex:309
#, elixir-format
msgid "Your account is currently disabled"
-msgstr ""
+msgstr "Il tuo profilo è attualmente disabilitato"
#: lib/pleroma/web/oauth/oauth_controller.ex:180
#: lib/pleroma/web/oauth/oauth_controller.ex:332
#, elixir-format
msgid "Your login is missing a confirmed e-mail address"
-msgstr ""
+msgstr "Devi aggiungere un indirizzo email valido"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
-msgstr ""
+msgstr "non puoi leggere i messaggi privati di %{nickname} come %{as_nickname}"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
-msgstr ""
+msgstr "non puoi aggiornare gli inviati di %{nickname} come %{as_nickname}"
#: lib/pleroma/web/common_api/common_api.ex:388
#, elixir-format
msgid "conversation is already muted"
-msgstr ""
+msgstr "la conversazione è già zittita"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
#, elixir-format
msgid "error"
-msgstr ""
+msgstr "errore"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29
#, elixir-format
msgid "mascots can only be images"
-msgstr ""
+msgstr "le mascotte possono solo essere immagini"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60
#, elixir-format
msgid "not found"
-msgstr ""
+msgstr "non trovato"
#: lib/pleroma/web/oauth/oauth_controller.ex:395
#, elixir-format
msgid "Bad OAuth request."
-msgstr ""
+msgstr "Richiesta OAuth malformata."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format
msgid "CAPTCHA already used"
-msgstr ""
+msgstr "CAPTCHA già utilizzato"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format
msgid "CAPTCHA expired"
-msgstr ""
+msgstr "CAPTCHA scaduto"
#: lib/pleroma/plugs/uploaded_media.ex:55
#, elixir-format
msgid "Failed"
-msgstr ""
+msgstr "Fallito"
#: lib/pleroma/web/oauth/oauth_controller.ex:411
#, elixir-format
msgid "Failed to authenticate: %{message}."
-msgstr ""
+msgstr "Autenticazione fallita per: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:442
#, elixir-format
msgid "Failed to set up user account."
-msgstr ""
+msgstr "Profilo utente non creato."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
-msgstr ""
+msgstr "Permessi insufficienti: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:94
#, elixir-format
msgid "Internal Error"
-msgstr ""
+msgstr "Errore interno"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
-msgstr ""
+msgstr "Nome utente/parola d'ordine invalidi"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
msgid "Invalid answer data"
-msgstr ""
+msgstr "Risposta malformata"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128
#, elixir-format
msgid "Nodeinfo schema version not handled"
-msgstr ""
+msgstr "Versione schema nodeinfo non compatibile"
#: lib/pleroma/web/oauth/oauth_controller.ex:169
#, elixir-format
msgid "This action is outside the authorized scopes"
-msgstr ""
+msgstr "Quest'azione non è consentita in questa visibilità"
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
-msgstr ""
+msgstr "Errore sconosciuto, controlla i dettagli e riprova."
#: lib/pleroma/web/oauth/oauth_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:155
#, elixir-format
msgid "Unlisted redirect_uri."
-msgstr ""
+msgstr "redirect_uri nascosto."
#: lib/pleroma/web/oauth/oauth_controller.ex:391
#, elixir-format
msgid "Unsupported OAuth provider: %{provider}."
-msgstr ""
+msgstr "Gestore OAuth non supportato: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format
msgid "Uploader callback timeout"
-msgstr ""
+msgstr "Callback caricatmento scaduta"
#: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format
msgid "bad request"
-msgstr ""
+msgstr "richiesta malformata"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
-msgstr ""
+msgstr "Errore CAPTCHA"
#: lib/pleroma/web/common_api/common_api.ex:200
#, elixir-format
msgid "Could not add reaction emoji"
-msgstr ""
+msgstr "Reazione emoji non riuscita"
#: lib/pleroma/web/common_api/common_api.ex:211
#, elixir-format
msgid "Could not remove reaction emoji"
-msgstr ""
+msgstr "Rimozione reazione non riuscita"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})"
-msgstr ""
+msgstr "CAPTCHA invalido (Parametro mancante: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
-msgstr ""
+msgstr "Lista non trovata"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124
#, elixir-format
msgid "Missing parameter: %{name}"
-msgstr ""
+msgstr "Parametro mancante: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:207
#: lib/pleroma/web/oauth/oauth_controller.ex:322
#, elixir-format
msgid "Password reset is required"
-msgstr ""
+msgstr "Necessario reimpostare parola d'ordine"
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6
@@ -528,53 +528,58 @@ msgstr ""
#, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr ""
+"Sicurezza violata: il controllo autorizzazioni di OAuth non è stato svolto "
+"né saltato."
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
+"Autenticazione bifattoriale abilitata, devi utilizzare una chiave d'accesso."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210
#, elixir-format
msgid "Unexpected error occurred while adding file to pack."
-msgstr ""
+msgstr "Errore inaspettato durante l'aggiunta del file al pacchetto."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138
#, elixir-format
msgid "Unexpected error occurred while creating pack."
-msgstr ""
+msgstr "Errore inaspettato durante la creazione del pacchetto."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278
#, elixir-format
msgid "Unexpected error occurred while removing file from pack."
-msgstr ""
+msgstr "Errore inaspettato durante la rimozione del file dal pacchetto."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250
#, elixir-format
msgid "Unexpected error occurred while updating file in pack."
-msgstr ""
+msgstr "Errore inaspettato durante l'aggiornamento del file nel pacchetto."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179
#, elixir-format
msgid "Unexpected error occurred while updating pack metadata."
-msgstr ""
+msgstr "Errore inaspettato durante l'aggiornamento dei metadati del pacchetto."
#: lib/pleroma/plugs/user_is_admin_plug.ex:40
#, elixir-format
msgid "User is not an admin or OAuth admin scope is not granted."
msgstr ""
+"L'utente non è un amministratore o non ha ricevuto questa autorizzazione "
+"OAuth."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
-msgstr ""
+msgstr "Gli aggiornamenti web push non sono disponibili in questa stanza"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:502
#, elixir-format
msgid "You can't revoke your own admin/moderator status."
-msgstr ""
+msgstr "Non puoi divestire te stesso."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105
#, elixir-format
msgid "authorization required for timeline view"
-msgstr ""
+msgstr "autorizzazione richiesta per vedere la sequenza"
diff --git a/priv/gettext/pl/LC_MESSAGES/errors.po b/priv/gettext/pl/LC_MESSAGES/errors.po
index 7bc39c52a..7241d8a0a 100644
--- a/priv/gettext/pl/LC_MESSAGES/errors.po
+++ b/priv/gettext/pl/LC_MESSAGES/errors.po
@@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-13 16:37+0000\n"
-"PO-Revision-Date: 2020-05-16 17:13+0000\n"
-"Last-Translator: Jędrzej Tomaszewski \n"
+"PO-Revision-Date: 2020-07-09 14:40+0000\n"
+"Last-Translator: Ben Is \n"
"Language-Team: Polish \n"
"Language: pl\n"
@@ -50,7 +50,7 @@ msgstr "jest zarezerwowany"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
-msgstr ""
+msgstr "nie pasuje do potwierdzenia"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
diff --git a/priv/repo/migrations/20200707112859_instances_add_favicon.exs b/priv/repo/migrations/20200707112859_instances_add_favicon.exs
new file mode 100644
index 000000000..5538749dc
--- /dev/null
+++ b/priv/repo/migrations/20200707112859_instances_add_favicon.exs
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.InstancesAddFavicon do
+ use Ecto.Migration
+
+ def change do
+ alter table(:instances) do
+ add(:favicon, :string)
+ add(:favicon_updated_at, :naive_datetime)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs b/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs
new file mode 100644
index 000000000..94efe323a
--- /dev/null
+++ b/priv/repo/migrations/20200708193702_drop_user_trigram_index.exs
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.DropUserTrigramIndex do
+ @moduledoc "Drops unused trigram index on `users` (FTS index is being used instead)"
+
+ use Ecto.Migration
+
+ def up do
+ drop_if_exists(index(:users, [], name: :users_trigram_index))
+ end
+
+ def down do
+ create_if_not_exists(
+ index(:users, ["(trim(nickname || ' ' || coalesce(name, ''))) gist_trgm_ops"],
+ name: :users_trigram_index,
+ using: :gist
+ )
+ )
+ end
+end
diff --git a/priv/static/index.html b/priv/static/index.html
index ddd4ec4eb..80820166a 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -1 +1 @@
-Pleroma To use Pleroma, please enable JavaScript.
\ No newline at end of file
+Pleroma To use Pleroma, please enable JavaScript.
\ No newline at end of file
diff --git a/priv/static/static-fe/static-fe.css b/priv/static/static-fe/static-fe.css
index db61ff266..89e9f4877 100644
Binary files a/priv/static/static-fe/static-fe.css and b/priv/static/static-fe/static-fe.css differ
diff --git a/priv/static/static/config.json b/priv/static/static/config.json
index 727dde73b..0030f78f1 100644
--- a/priv/static/static/config.json
+++ b/priv/static/static/config.json
@@ -14,7 +14,6 @@
"logoMargin": ".1em",
"logoMask": true,
"minimalScopesMode": false,
- "noAttachmentLinks": false,
"nsfwCensorImage": "",
"postContentType": "text/plain",
"redirectRootLogin": "/main/friends",
@@ -22,6 +21,7 @@
"scopeCopy": true,
"showFeaturesPanel": true,
"showInstanceSpecificPanel": false,
+ "sidebarRight": false,
"subjectLineBehavior": "email",
"theme": "pleroma-dark",
"webPushNotifications": false
diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css b/priv/static/static/css/2.0778a6a864a1307a6c41.css
new file mode 100644
index 000000000..a33585ef1
Binary files /dev/null and b/priv/static/static/css/2.0778a6a864a1307a6c41.css differ
diff --git a/priv/static/static/css/2.0778a6a864a1307a6c41.css.map b/priv/static/static/css/2.0778a6a864a1307a6c41.css.map
new file mode 100644
index 000000000..28cd8ba54
--- /dev/null
+++ b/priv/static/static/css/2.0778a6a864a1307a6c41.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/2.0778a6a864a1307a6c41.css","sourcesContent":[".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css b/priv/static/static/css/3.b2603a50868c68a1c192.css
similarity index 92%
rename from priv/static/static/css/vendors~app.18fea621d430000acc27.css
rename to priv/static/static/css/3.b2603a50868c68a1c192.css
index ef783cbb3..4cec5785b 100644
Binary files a/priv/static/static/css/vendors~app.18fea621d430000acc27.css and b/priv/static/static/css/3.b2603a50868c68a1c192.css differ
diff --git a/priv/static/static/css/3.b2603a50868c68a1c192.css.map b/priv/static/static/css/3.b2603a50868c68a1c192.css.map
new file mode 100644
index 000000000..805e7dc04
--- /dev/null
+++ b/priv/static/static/css/3.b2603a50868c68a1c192.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/3.b2603a50868c68a1c192.css","sourcesContent":["/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css b/priv/static/static/css/app.613cef07981cd95ccceb.css
deleted file mode 100644
index c1d5f8188..000000000
Binary files a/priv/static/static/css/app.613cef07981cd95ccceb.css and /dev/null differ
diff --git a/priv/static/static/css/app.613cef07981cd95ccceb.css.map b/priv/static/static/css/app.613cef07981cd95ccceb.css.map
deleted file mode 100644
index 556e0bb0b..000000000
--- a/priv/static/static/css/app.613cef07981cd95ccceb.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA,uBAAuB,aAAa,kBAAkB,qBAAqB,sBAAsB,qCAAqC,8BAA8B,e;ACApK,cAAc,oBAAoB,aAAa,0BAA0B,sBAAsB,wBAAwB,kBAAkB,cAAc,eAAe,gCAAgC,aAAa,wCAAwC,0BAA0B,aAAa,gBAAgB,oBAAoB,oBAAoB,aAAa,kBAAkB,WAAW,kBAAkB,gBAAgB,gBAAgB,sBAAsB,uDAAuD,cAAc,WAAW,kBAAkB,cAAc,wBAAwB,yBAAyB,wCAAwC,iCAAiC,YAAY,kBAAkB,oBAAoB,aAAa,kBAAkB,cAAc,sCAAsC,WAAW,cAAc,kBAAkB,4BAA4B,6BAA6B,gBAAgB,oBAAoB,oBAAoB,mBAAmB,cAAc,8BAA8B,yBAAyB,qCAAqC,mDAAmD,UAAU,yDAAyD,UAAU,6CAA6C,uBAAuB,UAAU,cAAc,oCAAoC,0CAA0C,gBAAgB,mBAAmB,gBAAgB,qDAAqD,WAAW,kBAAkB,OAAO,QAAQ,SAAS,UAAU,wBAAwB,yBAAyB,wC;ACAtlD,2BAA2B,aAAa,kBAAkB,kCAAkC,e","file":"static/css/app.613cef07981cd95ccceb.css","sourcesContent":[".with-load-more-footer{padding:10px;text-align:center;border-top:1px solid;border-top-color:#222;border-top-color:var(--border, #222)}.with-load-more-footer .error{font-size:14px}",".tab-switcher{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.tab-switcher .contents{-ms-flex:1 0 auto;flex:1 0 auto;min-height:0px}.tab-switcher .contents .hidden{display:none}.tab-switcher .contents.scrollable-tabs{-ms-flex-preferred-size:0;flex-basis:0;overflow-y:auto}.tab-switcher .tabs{display:-ms-flexbox;display:flex;position:relative;width:100%;overflow-y:hidden;overflow-x:auto;padding-top:5px;box-sizing:border-box}.tab-switcher .tabs::after,.tab-switcher .tabs::before{display:block;content:\"\";-ms-flex:1 1 auto;flex:1 1 auto;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}.tab-switcher .tabs .tab-wrapper{height:28px;position:relative;display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto}.tab-switcher .tabs .tab-wrapper .tab{width:100%;min-width:1px;position:relative;border-bottom-left-radius:0;border-bottom-right-radius:0;padding:6px 1em;padding-bottom:99px;margin-bottom:-93px;white-space:nowrap;color:#b9b9ba;color:var(--tabText, #b9b9ba);background-color:#182230;background-color:var(--tab, #182230)}.tab-switcher .tabs .tab-wrapper .tab:not(.active){z-index:4}.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover{z-index:6}.tab-switcher .tabs .tab-wrapper .tab.active{background:transparent;z-index:5;color:#b9b9ba;color:var(--tabActiveText, #b9b9ba)}.tab-switcher .tabs .tab-wrapper .tab img{max-height:26px;vertical-align:top;margin-top:-5px}.tab-switcher .tabs .tab-wrapper:not(.active)::after{content:\"\";position:absolute;left:0;right:0;bottom:0;z-index:7;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)}",".with-subscription-loading{padding:10px;text-align:center}.with-subscription-loading .error{font-size:14px}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/app.77b1644622e3bae24b6b.css b/priv/static/static/css/app.77b1644622e3bae24b6b.css
new file mode 100644
index 000000000..8038882c0
Binary files /dev/null and b/priv/static/static/css/app.77b1644622e3bae24b6b.css differ
diff --git a/priv/static/static/css/app.77b1644622e3bae24b6b.css.map b/priv/static/static/css/app.77b1644622e3bae24b6b.css.map
new file mode 100644
index 000000000..4b042ef35
--- /dev/null
+++ b/priv/static/static/css/app.77b1644622e3bae24b6b.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACtOA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.77b1644622e3bae24b6b.css","sourcesContent":[".tab-switcher {\n display: -ms-flexbox;\n display: flex;\n}\n.tab-switcher .tab-icon {\n font-size: 2em;\n display: block;\n}\n.tab-switcher.top-tabs {\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.top-tabs > .tabs {\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n -ms-flex-direction: row;\n flex-direction: row;\n}\n.tab-switcher.top-tabs > .tabs::after, .tab-switcher.top-tabs > .tabs::before {\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper {\n height: 28px;\n}\n.tab-switcher.top-tabs > .tabs .tab-wrapper:not(.active)::after {\n left: 0;\n right: 0;\n bottom: 0;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher.top-tabs > .tabs .tab {\n width: 100%;\n min-width: 1px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding-bottom: 99px;\n margin-bottom: -93px;\n}\n.tab-switcher.top-tabs .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n}\n.tab-switcher.side-tabs {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs {\n overflow-x: auto;\n }\n}\n.tab-switcher.side-tabs > .contents {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher.side-tabs > .tabs {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n overflow-y: auto;\n overflow-x: hidden;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher.side-tabs > .tabs::after, .tab-switcher.side-tabs > .tabs::before {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n -ms-flex-preferred-size: 0.5em;\n flex-basis: 0.5em;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs::after {\n -ms-flex-positive: 1;\n flex-grow: 1;\n}\n.tab-switcher.side-tabs > .tabs::before {\n -ms-flex-positive: 0;\n flex-grow: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 10em;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab-wrapper {\n min-width: 1em;\n }\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:not(.active)::after {\n top: 0;\n right: 0;\n bottom: 0;\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper::before {\n -ms-flex: 0 0 6px;\n flex: 0 0 6px;\n content: \"\";\n border-right: 1px solid;\n border-right-color: #222;\n border-right-color: var(--border, #222);\n}\n.tab-switcher.side-tabs > .tabs .tab-wrapper:last-child .tab {\n margin-bottom: 0;\n}\n.tab-switcher.side-tabs > .tabs .tab {\n -ms-flex: 1;\n flex: 1;\n box-sizing: content-box;\n min-width: 10em;\n min-width: 1px;\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n padding-left: 1em;\n padding-right: calc(1em + 200px);\n margin-right: -200px;\n margin-left: 1em;\n}\n@media all and (max-width: 800px) {\n .tab-switcher.side-tabs > .tabs .tab {\n padding-left: 0.25em;\n padding-right: calc(.25em + 200px);\n margin-right: calc(.25em - 200px);\n margin-left: 0.25em;\n }\n .tab-switcher.side-tabs > .tabs .tab .text {\n display: none;\n }\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents .full-height:not(.hidden) {\n height: 100%;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents .full-height:not(.hidden) > *:not(.mobile-label) {\n -ms-flex: 1;\n flex: 1;\n}\n.tab-switcher .contents.scrollable-tabs {\n overflow-y: auto;\n}\n.tab-switcher .tab {\n position: relative;\n white-space: nowrap;\n padding: 6px 1em;\n background-color: #182230;\n background-color: var(--tab, #182230);\n}\n.tab-switcher .tab, .tab-switcher .tab:active .tab-icon {\n color: #b9b9ba;\n color: var(--tabText, #b9b9ba);\n}\n.tab-switcher .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tab.active {\n background: transparent;\n z-index: 5;\n color: #b9b9ba;\n color: var(--tabActiveText, #b9b9ba);\n}\n.tab-switcher .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n.tab-switcher .tab-wrapper {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n z-index: 7;\n}\n.tab-switcher .mobile-label {\n padding-left: 0.3em;\n padding-bottom: 0.25em;\n margin-top: 0.5em;\n margin-left: 0.2em;\n margin-bottom: 0.25em;\n border-bottom: 1px solid var(--border, #222);\n}\n@media all and (min-width: 800px) {\n .tab-switcher .mobile-label {\n display: none;\n }\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}\n.with-load-more-footer a {\n cursor: pointer;\n}"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map b/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map
deleted file mode 100644
index 057d67d6a..000000000
--- a/priv/static/static/css/vendors~app.18fea621d430000acc27.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["webpack:///./node_modules/cropperjs/dist/cropper.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,wCAAwC;AACxC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA","file":"static/css/vendors~app.18fea621d430000acc27.css","sourcesContent":["/*!\n * Cropper.js v1.5.6\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2019-10-04T04:33:44.164Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: 0.5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline: 1px solid #39f;\n outline-color: rgba(51, 153, 255, 0.75);\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: 0.5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: 0.75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center::before,\n.cropper-center::after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center::before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center::after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: 0.1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: 0.75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: 0.75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se::before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/priv/static/static/font/fontello.1589385935077.woff b/priv/static/static/font/fontello.1589385935077.woff
deleted file mode 100644
index f48488a77..000000000
Binary files a/priv/static/static/font/fontello.1589385935077.woff and /dev/null differ
diff --git a/priv/static/static/font/fontello.1589385935077.woff2 b/priv/static/static/font/fontello.1589385935077.woff2
deleted file mode 100644
index 012eb9305..000000000
Binary files a/priv/static/static/font/fontello.1589385935077.woff2 and /dev/null differ
diff --git a/priv/static/static/font/fontello.1589385935077.ttf b/priv/static/static/font/fontello.1594374054351.eot
similarity index 80%
rename from priv/static/static/font/fontello.1589385935077.ttf
rename to priv/static/static/font/fontello.1594374054351.eot
index 0fde96cea..62b619386 100644
Binary files a/priv/static/static/font/fontello.1589385935077.ttf and b/priv/static/static/font/fontello.1594374054351.eot differ
diff --git a/priv/static/static/font/fontello.1589385935077.svg b/priv/static/static/font/fontello.1594374054351.svg
similarity index 92%
rename from priv/static/static/font/fontello.1589385935077.svg
rename to priv/static/static/font/fontello.1594374054351.svg
index e63fb7529..71b5d70af 100644
--- a/priv/static/static/font/fontello.1589385935077.svg
+++ b/priv/static/static/font/fontello.1594374054351.svg
@@ -80,8 +80,18 @@
+
+
+
+
+
+
+
+
+
+
@@ -90,6 +100,10 @@
+
+
+
+
diff --git a/priv/static/static/font/fontello.1589385935077.eot b/priv/static/static/font/fontello.1594374054351.ttf
similarity index 80%
rename from priv/static/static/font/fontello.1589385935077.eot
rename to priv/static/static/font/fontello.1594374054351.ttf
index e5f37013a..be55bef81 100644
Binary files a/priv/static/static/font/fontello.1589385935077.eot and b/priv/static/static/font/fontello.1594374054351.ttf differ
diff --git a/priv/static/static/font/fontello.1594374054351.woff b/priv/static/static/font/fontello.1594374054351.woff
new file mode 100644
index 000000000..115945f70
Binary files /dev/null and b/priv/static/static/font/fontello.1594374054351.woff differ
diff --git a/priv/static/static/font/fontello.1594374054351.woff2 b/priv/static/static/font/fontello.1594374054351.woff2
new file mode 100644
index 000000000..cb214aab3
Binary files /dev/null and b/priv/static/static/font/fontello.1594374054351.woff2 differ
diff --git a/priv/static/static/fontello.1594030805019.css b/priv/static/static/fontello.1594030805019.css
new file mode 100644
index 000000000..9251070fe
Binary files /dev/null and b/priv/static/static/fontello.1594030805019.css differ
diff --git a/priv/static/static/fontello.1594134783339.css b/priv/static/static/fontello.1594134783339.css
new file mode 100644
index 000000000..ff35edaba
Binary files /dev/null and b/priv/static/static/fontello.1594134783339.css differ
diff --git a/priv/static/static/fontello.1594374054351.css b/priv/static/static/fontello.1594374054351.css
new file mode 100644
index 000000000..6dea8ee3e
Binary files /dev/null and b/priv/static/static/fontello.1594374054351.css differ
diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json
index 7f0e7cdd5..706800cdb 100755
--- a/priv/static/static/fontello.json
+++ b/priv/static/static/fontello.json
@@ -363,6 +363,48 @@
"css": "ok",
"code": 59431,
"src": "fontawesome"
+ },
+ {
+ "uid": "4109c474ff99cad28fd5a2c38af2ec6f",
+ "css": "filter",
+ "code": 61616,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
+ "css": "download",
+ "code": 59429,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f04a5d24e9e659145b966739c4fde82a",
+ "css": "bookmark",
+ "code": 59430,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2f5ef6f6b7aaebc56458ab4e865beff5",
+ "css": "bookmark-empty",
+ "code": 61591,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9ea0a737ccc45d6c510dcbae56058849",
+ "css": "music",
+ "code": 59432,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "1b5a5d7b7e3c71437f5a26befdd045ed",
+ "css": "doc",
+ "code": 59433,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+ "css": "block",
+ "code": 59434,
+ "src": "fontawesome"
}
]
}
\ No newline at end of file
diff --git a/priv/static/static/js/10.2823375ec309b971aaea.js b/priv/static/static/js/10.2823375ec309b971aaea.js
new file mode 100644
index 000000000..8f34c42ea
Binary files /dev/null and b/priv/static/static/js/10.2823375ec309b971aaea.js differ
diff --git a/priv/static/static/js/10.2823375ec309b971aaea.js.map b/priv/static/static/js/10.2823375ec309b971aaea.js.map
new file mode 100644
index 000000000..8933e2336
Binary files /dev/null and b/priv/static/static/js/10.2823375ec309b971aaea.js.map differ
diff --git a/priv/static/static/js/11.2cb4b0f72a4654070a58.js b/priv/static/static/js/11.2cb4b0f72a4654070a58.js
new file mode 100644
index 000000000..03b0234f2
Binary files /dev/null and b/priv/static/static/js/11.2cb4b0f72a4654070a58.js differ
diff --git a/priv/static/static/js/11.2cb4b0f72a4654070a58.js.map b/priv/static/static/js/11.2cb4b0f72a4654070a58.js.map
new file mode 100644
index 000000000..b53e5e23a
Binary files /dev/null and b/priv/static/static/js/11.2cb4b0f72a4654070a58.js.map differ
diff --git a/priv/static/static/js/12.500b3e4676dd47599a58.js b/priv/static/static/js/12.500b3e4676dd47599a58.js
new file mode 100644
index 000000000..52dfbde92
Binary files /dev/null and b/priv/static/static/js/12.500b3e4676dd47599a58.js differ
diff --git a/priv/static/static/js/12.500b3e4676dd47599a58.js.map b/priv/static/static/js/12.500b3e4676dd47599a58.js.map
new file mode 100644
index 000000000..700da90b0
Binary files /dev/null and b/priv/static/static/js/12.500b3e4676dd47599a58.js.map differ
diff --git a/priv/static/static/js/13.3ef79a2643680080d28f.js b/priv/static/static/js/13.3ef79a2643680080d28f.js
new file mode 100644
index 000000000..4070d1f3f
Binary files /dev/null and b/priv/static/static/js/13.3ef79a2643680080d28f.js differ
diff --git a/priv/static/static/js/13.3ef79a2643680080d28f.js.map b/priv/static/static/js/13.3ef79a2643680080d28f.js.map
new file mode 100644
index 000000000..bb2f24e3a
Binary files /dev/null and b/priv/static/static/js/13.3ef79a2643680080d28f.js.map differ
diff --git a/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js b/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js
new file mode 100644
index 000000000..316ba1291
Binary files /dev/null and b/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js differ
diff --git a/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js.map b/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js.map
new file mode 100644
index 000000000..07a26f298
Binary files /dev/null and b/priv/static/static/js/14.b7f6eb3ea71d2ac2bb41.js.map differ
diff --git a/priv/static/static/js/15.d814a29a970070494722.js b/priv/static/static/js/15.d814a29a970070494722.js
new file mode 100644
index 000000000..17eaf5218
Binary files /dev/null and b/priv/static/static/js/15.d814a29a970070494722.js differ
diff --git a/priv/static/static/js/15.d814a29a970070494722.js.map b/priv/static/static/js/15.d814a29a970070494722.js.map
new file mode 100644
index 000000000..9792088bf
Binary files /dev/null and b/priv/static/static/js/15.d814a29a970070494722.js.map differ
diff --git a/priv/static/static/js/16.017fa510b293035ac370.js b/priv/static/static/js/16.017fa510b293035ac370.js
new file mode 100644
index 000000000..387cfc9c7
Binary files /dev/null and b/priv/static/static/js/16.017fa510b293035ac370.js differ
diff --git a/priv/static/static/js/16.017fa510b293035ac370.js.map b/priv/static/static/js/16.017fa510b293035ac370.js.map
new file mode 100644
index 000000000..2886028bd
Binary files /dev/null and b/priv/static/static/js/16.017fa510b293035ac370.js.map differ
diff --git a/priv/static/static/js/17.c63932b65417ee7346a3.js b/priv/static/static/js/17.c63932b65417ee7346a3.js
new file mode 100644
index 000000000..e3172472a
Binary files /dev/null and b/priv/static/static/js/17.c63932b65417ee7346a3.js differ
diff --git a/priv/static/static/js/17.c63932b65417ee7346a3.js.map b/priv/static/static/js/17.c63932b65417ee7346a3.js.map
new file mode 100644
index 000000000..f4c55d0cc
Binary files /dev/null and b/priv/static/static/js/17.c63932b65417ee7346a3.js.map differ
diff --git a/priv/static/static/js/18.fd12f9746a55aa24a8b7.js b/priv/static/static/js/18.fd12f9746a55aa24a8b7.js
new file mode 100644
index 000000000..be1ecbba5
Binary files /dev/null and b/priv/static/static/js/18.fd12f9746a55aa24a8b7.js differ
diff --git a/priv/static/static/js/18.fd12f9746a55aa24a8b7.js.map b/priv/static/static/js/18.fd12f9746a55aa24a8b7.js.map
new file mode 100644
index 000000000..c98c107b3
Binary files /dev/null and b/priv/static/static/js/18.fd12f9746a55aa24a8b7.js.map differ
diff --git a/priv/static/static/js/19.3adebd64964c92700074.js b/priv/static/static/js/19.3adebd64964c92700074.js
new file mode 100644
index 000000000..9d5adbe4e
Binary files /dev/null and b/priv/static/static/js/19.3adebd64964c92700074.js differ
diff --git a/priv/static/static/js/19.3adebd64964c92700074.js.map b/priv/static/static/js/19.3adebd64964c92700074.js.map
new file mode 100644
index 000000000..d113a66dc
Binary files /dev/null and b/priv/static/static/js/19.3adebd64964c92700074.js.map differ
diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js b/priv/static/static/js/2.18e4adec273c4ce867a8.js
deleted file mode 100644
index d191aa852..000000000
Binary files a/priv/static/static/js/2.18e4adec273c4ce867a8.js and /dev/null differ
diff --git a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map b/priv/static/static/js/2.18e4adec273c4ce867a8.js.map
deleted file mode 100644
index a7f98bfef..000000000
Binary files a/priv/static/static/js/2.18e4adec273c4ce867a8.js.map and /dev/null differ
diff --git a/priv/static/static/js/2.d81ca020d6885c6c3b03.js b/priv/static/static/js/2.d81ca020d6885c6c3b03.js
new file mode 100644
index 000000000..f751a05da
Binary files /dev/null and b/priv/static/static/js/2.d81ca020d6885c6c3b03.js differ
diff --git a/priv/static/static/js/2.d81ca020d6885c6c3b03.js.map b/priv/static/static/js/2.d81ca020d6885c6c3b03.js.map
new file mode 100644
index 000000000..9a675dbc5
Binary files /dev/null and b/priv/static/static/js/2.d81ca020d6885c6c3b03.js.map differ
diff --git a/priv/static/static/js/20.e0c3ad29d59470506c04.js b/priv/static/static/js/20.e0c3ad29d59470506c04.js
new file mode 100644
index 000000000..ddedbd1ff
Binary files /dev/null and b/priv/static/static/js/20.e0c3ad29d59470506c04.js differ
diff --git a/priv/static/static/js/20.e0c3ad29d59470506c04.js.map b/priv/static/static/js/20.e0c3ad29d59470506c04.js.map
new file mode 100644
index 000000000..83a9fbc98
Binary files /dev/null and b/priv/static/static/js/20.e0c3ad29d59470506c04.js.map differ
diff --git a/priv/static/static/js/21.849ecc09a1d58bdc64c6.js b/priv/static/static/js/21.849ecc09a1d58bdc64c6.js
new file mode 100644
index 000000000..ef58a3da1
Binary files /dev/null and b/priv/static/static/js/21.849ecc09a1d58bdc64c6.js differ
diff --git a/priv/static/static/js/21.849ecc09a1d58bdc64c6.js.map b/priv/static/static/js/21.849ecc09a1d58bdc64c6.js.map
new file mode 100644
index 000000000..9447b7ce3
Binary files /dev/null and b/priv/static/static/js/21.849ecc09a1d58bdc64c6.js.map differ
diff --git a/priv/static/static/js/22.8782f133c9f66d3f2bbe.js b/priv/static/static/js/22.8782f133c9f66d3f2bbe.js
new file mode 100644
index 000000000..82692acdb
Binary files /dev/null and b/priv/static/static/js/22.8782f133c9f66d3f2bbe.js differ
diff --git a/priv/static/static/js/22.8782f133c9f66d3f2bbe.js.map b/priv/static/static/js/22.8782f133c9f66d3f2bbe.js.map
new file mode 100644
index 000000000..41e527ff6
Binary files /dev/null and b/priv/static/static/js/22.8782f133c9f66d3f2bbe.js.map differ
diff --git a/priv/static/static/js/23.2653bf91bc77c2ed0160.js b/priv/static/static/js/23.2653bf91bc77c2ed0160.js
new file mode 100644
index 000000000..2aad331b4
Binary files /dev/null and b/priv/static/static/js/23.2653bf91bc77c2ed0160.js differ
diff --git a/priv/static/static/js/23.2653bf91bc77c2ed0160.js.map b/priv/static/static/js/23.2653bf91bc77c2ed0160.js.map
new file mode 100644
index 000000000..4f031922e
Binary files /dev/null and b/priv/static/static/js/23.2653bf91bc77c2ed0160.js.map differ
diff --git a/priv/static/static/js/24.f931d864a2297d880a9a.js b/priv/static/static/js/24.f931d864a2297d880a9a.js
new file mode 100644
index 000000000..0362730e0
Binary files /dev/null and b/priv/static/static/js/24.f931d864a2297d880a9a.js differ
diff --git a/priv/static/static/js/24.f931d864a2297d880a9a.js.map b/priv/static/static/js/24.f931d864a2297d880a9a.js.map
new file mode 100644
index 000000000..2fb375e79
Binary files /dev/null and b/priv/static/static/js/24.f931d864a2297d880a9a.js.map differ
diff --git a/priv/static/static/js/25.886acc9ba83c64659279.js b/priv/static/static/js/25.886acc9ba83c64659279.js
new file mode 100644
index 000000000..4ff4c331b
Binary files /dev/null and b/priv/static/static/js/25.886acc9ba83c64659279.js differ
diff --git a/priv/static/static/js/25.886acc9ba83c64659279.js.map b/priv/static/static/js/25.886acc9ba83c64659279.js.map
new file mode 100644
index 000000000..c39f71238
Binary files /dev/null and b/priv/static/static/js/25.886acc9ba83c64659279.js.map differ
diff --git a/priv/static/static/js/26.e15b1645079c72c60586.js b/priv/static/static/js/26.e15b1645079c72c60586.js
new file mode 100644
index 000000000..303170088
Binary files /dev/null and b/priv/static/static/js/26.e15b1645079c72c60586.js differ
diff --git a/priv/static/static/js/26.e15b1645079c72c60586.js.map b/priv/static/static/js/26.e15b1645079c72c60586.js.map
new file mode 100644
index 000000000..e62345884
Binary files /dev/null and b/priv/static/static/js/26.e15b1645079c72c60586.js.map differ
diff --git a/priv/static/static/js/27.7b41e5953f74af7fddd1.js b/priv/static/static/js/27.7b41e5953f74af7fddd1.js
new file mode 100644
index 000000000..769fba11b
Binary files /dev/null and b/priv/static/static/js/27.7b41e5953f74af7fddd1.js differ
diff --git a/priv/static/static/js/27.7b41e5953f74af7fddd1.js.map b/priv/static/static/js/27.7b41e5953f74af7fddd1.js.map
new file mode 100644
index 000000000..078f5ff9a
Binary files /dev/null and b/priv/static/static/js/27.7b41e5953f74af7fddd1.js.map differ
diff --git a/priv/static/static/js/28.4f39e562aaceaa01e883.js b/priv/static/static/js/28.4f39e562aaceaa01e883.js
new file mode 100644
index 000000000..629359bda
Binary files /dev/null and b/priv/static/static/js/28.4f39e562aaceaa01e883.js differ
diff --git a/priv/static/static/js/28.4f39e562aaceaa01e883.js.map b/priv/static/static/js/28.4f39e562aaceaa01e883.js.map
new file mode 100644
index 000000000..24c675a4c
Binary files /dev/null and b/priv/static/static/js/28.4f39e562aaceaa01e883.js.map differ
diff --git a/priv/static/static/js/29.137e2a68b558eed58152.js b/priv/static/static/js/29.137e2a68b558eed58152.js
new file mode 100644
index 000000000..50cb11ffd
Binary files /dev/null and b/priv/static/static/js/29.137e2a68b558eed58152.js differ
diff --git a/priv/static/static/js/29.137e2a68b558eed58152.js.map b/priv/static/static/js/29.137e2a68b558eed58152.js.map
new file mode 100644
index 000000000..0ac2f7fd3
Binary files /dev/null and b/priv/static/static/js/29.137e2a68b558eed58152.js.map differ
diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js
new file mode 100644
index 000000000..6b20ecb04
Binary files /dev/null and b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js differ
diff --git a/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map
new file mode 100644
index 000000000..594d9047b
Binary files /dev/null and b/priv/static/static/js/3.56898c1005d9ba1b8d4a.js.map differ
diff --git a/priv/static/static/js/30.73e09f3b43617410dec7.js b/priv/static/static/js/30.73e09f3b43617410dec7.js
new file mode 100644
index 000000000..0c3d03cfa
Binary files /dev/null and b/priv/static/static/js/30.73e09f3b43617410dec7.js differ
diff --git a/priv/static/static/js/30.73e09f3b43617410dec7.js.map b/priv/static/static/js/30.73e09f3b43617410dec7.js.map
new file mode 100644
index 000000000..cb546de17
Binary files /dev/null and b/priv/static/static/js/30.73e09f3b43617410dec7.js.map differ
diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js b/priv/static/static/js/4.2d3bef896b463484e6eb.js
new file mode 100644
index 000000000..4a611feb4
Binary files /dev/null and b/priv/static/static/js/4.2d3bef896b463484e6eb.js differ
diff --git a/priv/static/static/js/4.2d3bef896b463484e6eb.js.map b/priv/static/static/js/4.2d3bef896b463484e6eb.js.map
new file mode 100644
index 000000000..ebcc883e5
Binary files /dev/null and b/priv/static/static/js/4.2d3bef896b463484e6eb.js.map differ
diff --git a/priv/static/static/js/5.2b4a2787bacdd3d910db.js b/priv/static/static/js/5.2b4a2787bacdd3d910db.js
new file mode 100644
index 000000000..18c059380
Binary files /dev/null and b/priv/static/static/js/5.2b4a2787bacdd3d910db.js differ
diff --git a/priv/static/static/js/5.2b4a2787bacdd3d910db.js.map b/priv/static/static/js/5.2b4a2787bacdd3d910db.js.map
new file mode 100644
index 000000000..e9e78632d
Binary files /dev/null and b/priv/static/static/js/5.2b4a2787bacdd3d910db.js.map differ
diff --git a/priv/static/static/js/6.9c94bc0cc78979694cf4.js b/priv/static/static/js/6.9c94bc0cc78979694cf4.js
new file mode 100644
index 000000000..415938f67
Binary files /dev/null and b/priv/static/static/js/6.9c94bc0cc78979694cf4.js differ
diff --git a/priv/static/static/js/6.9c94bc0cc78979694cf4.js.map b/priv/static/static/js/6.9c94bc0cc78979694cf4.js.map
new file mode 100644
index 000000000..948368f60
Binary files /dev/null and b/priv/static/static/js/6.9c94bc0cc78979694cf4.js.map differ
diff --git a/priv/static/static/js/7.b4ac57fd946a3a189047.js b/priv/static/static/js/7.b4ac57fd946a3a189047.js
new file mode 100644
index 000000000..18b6ab76c
Binary files /dev/null and b/priv/static/static/js/7.b4ac57fd946a3a189047.js differ
diff --git a/priv/static/static/js/7.b4ac57fd946a3a189047.js.map b/priv/static/static/js/7.b4ac57fd946a3a189047.js.map
new file mode 100644
index 000000000..054d52650
Binary files /dev/null and b/priv/static/static/js/7.b4ac57fd946a3a189047.js.map differ
diff --git a/priv/static/static/js/8.e03e32ca713d01db0433.js b/priv/static/static/js/8.e03e32ca713d01db0433.js
new file mode 100644
index 000000000..4d5894322
Binary files /dev/null and b/priv/static/static/js/8.e03e32ca713d01db0433.js differ
diff --git a/priv/static/static/js/8.e03e32ca713d01db0433.js.map b/priv/static/static/js/8.e03e32ca713d01db0433.js.map
new file mode 100644
index 000000000..d1385c203
Binary files /dev/null and b/priv/static/static/js/8.e03e32ca713d01db0433.js.map differ
diff --git a/priv/static/static/js/9.72d903ca8e0c5a532b87.js b/priv/static/static/js/9.72d903ca8e0c5a532b87.js
new file mode 100644
index 000000000..ce0f066c5
Binary files /dev/null and b/priv/static/static/js/9.72d903ca8e0c5a532b87.js differ
diff --git a/priv/static/static/js/9.72d903ca8e0c5a532b87.js.map b/priv/static/static/js/9.72d903ca8e0c5a532b87.js.map
new file mode 100644
index 000000000..4cf79de5b
Binary files /dev/null and b/priv/static/static/js/9.72d903ca8e0c5a532b87.js.map differ
diff --git a/priv/static/static/js/app.1e68e208590653dab5aa.js b/priv/static/static/js/app.1e68e208590653dab5aa.js
new file mode 100644
index 000000000..27cc3d910
Binary files /dev/null and b/priv/static/static/js/app.1e68e208590653dab5aa.js differ
diff --git a/priv/static/static/js/app.1e68e208590653dab5aa.js.map b/priv/static/static/js/app.1e68e208590653dab5aa.js.map
new file mode 100644
index 000000000..71636d936
Binary files /dev/null and b/priv/static/static/js/app.1e68e208590653dab5aa.js.map differ
diff --git a/priv/static/static/js/app.838ffa9aecf210c7d744.js b/priv/static/static/js/app.838ffa9aecf210c7d744.js
deleted file mode 100644
index 7e224748e..000000000
Binary files a/priv/static/static/js/app.838ffa9aecf210c7d744.js and /dev/null differ
diff --git a/priv/static/static/js/app.838ffa9aecf210c7d744.js.map b/priv/static/static/js/app.838ffa9aecf210c7d744.js.map
deleted file mode 100644
index 4c2835cb4..000000000
Binary files a/priv/static/static/js/app.838ffa9aecf210c7d744.js.map and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js b/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js
new file mode 100644
index 000000000..bf6671e4b
Binary files /dev/null and b/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js differ
diff --git a/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js.map b/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js.map
new file mode 100644
index 000000000..2a3bf1b99
Binary files /dev/null and b/priv/static/static/js/vendors~app.247dc52c7abe6a0dab87.js.map differ
diff --git a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js b/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js
deleted file mode 100644
index d1f1a1830..000000000
Binary files a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js and /dev/null differ
diff --git a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map b/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map
deleted file mode 100644
index 0d4a859ea..000000000
Binary files a/priv/static/static/js/vendors~app.561a1c605d1dfb0e6f74.js.map and /dev/null differ
diff --git a/priv/static/static/terms-of-service.html b/priv/static/static/terms-of-service.html
index a6da539e4..3b6bbb36b 100644
--- a/priv/static/static/terms-of-service.html
+++ b/priv/static/static/terms-of-service.html
@@ -1,4 +1,9 @@
Terms of Service
-This is a placeholder ToS. Edit "/static/terms-of-service.html"
to make it fit the needs of your instance.
+This is the default placeholder ToS. You should copy it over to your static folder and edit it to fit the needs of your instance.
+
+To do so, place a file at "/instance/static/static/terms-of-service.html"
in your
+ Pleroma install containing the real ToS for your instance.
+See the Pleroma documentation for more information.
+
diff --git a/priv/static/static/themes/redmond-xx-se.json b/priv/static/static/themes/redmond-xx-se.json
index 7a4a29da3..24480d2c7 100644
--- a/priv/static/static/themes/redmond-xx-se.json
+++ b/priv/static/static/themes/redmond-xx-se.json
@@ -286,7 +286,9 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "0",
diff --git a/priv/static/static/themes/redmond-xx.json b/priv/static/static/themes/redmond-xx.json
index ff95b1e06..cf9010fe2 100644
--- a/priv/static/static/themes/redmond-xx.json
+++ b/priv/static/static/themes/redmond-xx.json
@@ -277,7 +277,9 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "0",
diff --git a/priv/static/static/themes/redmond-xxi.json b/priv/static/static/themes/redmond-xxi.json
index f788bdb83..7fdc4a6d6 100644
--- a/priv/static/static/themes/redmond-xxi.json
+++ b/priv/static/static/themes/redmond-xxi.json
@@ -259,7 +259,9 @@
"cGreen": "#669966",
"cOrange": "#cc6633",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "selectedMenu": "--accent",
+ "selectedMenuPopover": "--accent"
},
"radii": {
"btn": "0",
diff --git a/priv/static/sw-pleroma-workbox.js b/priv/static/sw-pleroma-workbox.js
new file mode 100644
index 000000000..0b39d0963
Binary files /dev/null and b/priv/static/sw-pleroma-workbox.js differ
diff --git a/priv/static/sw-pleroma-workbox.js.map b/priv/static/sw-pleroma-workbox.js.map
new file mode 100644
index 000000000..e35c07e72
Binary files /dev/null and b/priv/static/sw-pleroma-workbox.js.map differ
diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js
index 4d73c414e..098f58d49 100644
Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ
diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map
index c704cb951..5749809d5 100644
Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ
diff --git a/priv/static/sw.js b/priv/static/sw.js
index 0fde0f440..5605bb05e 100644
Binary files a/priv/static/sw.js and b/priv/static/sw.js differ
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index bc871a0a9..9082ae5a7 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -31,7 +31,7 @@ test "build report email" do
account_url
}\">#{account.nickname}
\nComment: Test comment\n
Statuses:\n
\n
\n\n"
+ }\">#{status_url}\n \n\n\n\nView Reports in AdminFE \n"
end
test "it works when the reporter is a remote user without email" do
diff --git a/test/filter_test.exs b/test/filter_test.exs
index 63a30c736..0a5c4426a 100644
--- a/test/filter_test.exs
+++ b/test/filter_test.exs
@@ -3,37 +3,39 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FilterTest do
- alias Pleroma.Repo
use Pleroma.DataCase
import Pleroma.Factory
+ alias Pleroma.Filter
+ alias Pleroma.Repo
+
describe "creating filters" do
test "creating one filter" do
user = insert(:user)
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
filter_id: 42,
phrase: "knights",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query)
- result = Pleroma.Filter.get(filter.filter_id, user)
+ {:ok, %Filter{} = filter} = Filter.create(query)
+ result = Filter.get(filter.filter_id, user)
assert query.phrase == result.phrase
end
test "creating one filter without a pre-defined filter_id" do
user = insert(:user)
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
phrase: "knights",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query)
+ {:ok, %Filter{} = filter} = Filter.create(query)
# Should start at 1
assert filter.filter_id == 1
end
@@ -41,23 +43,23 @@ test "creating one filter without a pre-defined filter_id" do
test "creating additional filters uses previous highest filter_id + 1" do
user = insert(:user)
- query_one = %Pleroma.Filter{
+ query_one = %Filter{
user_id: user.id,
filter_id: 42,
phrase: "knights",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one)
+ {:ok, %Filter{} = filter_one} = Filter.create(query_one)
- query_two = %Pleroma.Filter{
+ query_two = %Filter{
user_id: user.id,
# No filter_id
phrase: "who",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two)
+ {:ok, %Filter{} = filter_two} = Filter.create(query_two)
assert filter_two.filter_id == filter_one.filter_id + 1
end
@@ -65,29 +67,29 @@ test "filter_id is unique per user" do
user_one = insert(:user)
user_two = insert(:user)
- query_one = %Pleroma.Filter{
+ query_one = %Filter{
user_id: user_one.id,
phrase: "knights",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one)
+ {:ok, %Filter{} = filter_one} = Filter.create(query_one)
- query_two = %Pleroma.Filter{
+ query_two = %Filter{
user_id: user_two.id,
phrase: "who",
context: ["home"]
}
- {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two)
+ {:ok, %Filter{} = filter_two} = Filter.create(query_two)
assert filter_one.filter_id == 1
assert filter_two.filter_id == 1
- result_one = Pleroma.Filter.get(filter_one.filter_id, user_one)
+ result_one = Filter.get(filter_one.filter_id, user_one)
assert result_one.phrase == filter_one.phrase
- result_two = Pleroma.Filter.get(filter_two.filter_id, user_two)
+ result_two = Filter.get(filter_two.filter_id, user_two)
assert result_two.phrase == filter_two.phrase
end
end
@@ -95,38 +97,38 @@ test "filter_id is unique per user" do
test "deleting a filter" do
user = insert(:user)
- query = %Pleroma.Filter{
+ query = %Filter{
user_id: user.id,
filter_id: 0,
phrase: "knights",
context: ["home"]
}
- {:ok, _filter} = Pleroma.Filter.create(query)
- {:ok, filter} = Pleroma.Filter.delete(query)
- assert is_nil(Repo.get(Pleroma.Filter, filter.filter_id))
+ {:ok, _filter} = Filter.create(query)
+ {:ok, filter} = Filter.delete(query)
+ assert is_nil(Repo.get(Filter, filter.filter_id))
end
test "getting all filters by an user" do
user = insert(:user)
- query_one = %Pleroma.Filter{
+ query_one = %Filter{
user_id: user.id,
filter_id: 1,
phrase: "knights",
context: ["home"]
}
- query_two = %Pleroma.Filter{
+ query_two = %Filter{
user_id: user.id,
filter_id: 2,
phrase: "who",
context: ["home"]
}
- {:ok, filter_one} = Pleroma.Filter.create(query_one)
- {:ok, filter_two} = Pleroma.Filter.create(query_two)
- filters = Pleroma.Filter.get_filters(user)
+ {:ok, filter_one} = Filter.create(query_one)
+ {:ok, filter_two} = Filter.create(query_two)
+ filters = Filter.get_filters(user)
assert filter_one in filters
assert filter_two in filters
end
@@ -134,7 +136,7 @@ test "getting all filters by an user" do
test "updating a filter" do
user = insert(:user)
- query_one = %Pleroma.Filter{
+ query_one = %Filter{
user_id: user.id,
filter_id: 1,
phrase: "knights",
@@ -146,8 +148,9 @@ test "updating a filter" do
context: ["home", "timeline"]
}
- {:ok, filter_one} = Pleroma.Filter.create(query_one)
- {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes)
+ {:ok, filter_one} = Filter.create(query_one)
+ {:ok, filter_two} = Filter.update(filter_one, changes)
+
assert filter_one != filter_two
assert filter_two.phrase == changes.phrase
assert filter_two.context == changes.context
diff --git a/test/fixtures/tesla_mock/framatube.org-video.json b/test/fixtures/tesla_mock/framatube.org-video.json
new file mode 100644
index 000000000..3d53f0c97
--- /dev/null
+++ b/test/fixtures/tesla_mock/framatube.org-video.json
@@ -0,0 +1 @@
+{"type":"Video","id":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206","name":"Déframasoftisons Internet [Framasoft]","duration":"PT3622S","uuid":"6050732a-8a7a-43d4-a6cd-809525a1d206","tag":[{"type":"Hashtag","name":"déframasoftisons"},{"type":"Hashtag","name":"EPN23"},{"type":"Hashtag","name":"framaconf"},{"type":"Hashtag","name":"Framasoft"},{"type":"Hashtag","name":"pyg"}],"category":{"identifier":"15","name":"Science & Technology"},"views":122,"sensitive":false,"waitTranscoding":false,"state":1,"commentsEnabled":true,"downloadEnabled":true,"published":"2020-05-24T18:34:31.569Z","originallyPublishedAt":"2019-11-30T23:00:00.000Z","updated":"2020-07-05T09:01:01.720Z","mediaType":"text/markdown","content":"Après avoir mené avec un certain succès la campagne « Dégooglisons Internet » en 2014, l’association Framasoft annonce fin 2019 arrêter progressivement un certain nombre de ses services alternatifs aux GAFAM. Pourquoi ?\r\n\r\nTranscription par @april...","support":null,"subtitleLanguage":[],"icon":{"type":"Image","url":"https://framatube.org/static/thumbnails/6050732a-8a7a-43d4-a6cd-809525a1d206.jpg","mediaType":"image/jpeg","width":223,"height":122},"url":[{"type":"Link","mediaType":"text/html","href":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080,"size":1157359410,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309939","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.torrent&xt=urn:btih:381c9429900552e23a4eb506318f1fa01e4d63a8&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480,"size":250095131,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309941","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.torrent&xt=urn:btih:a181dcbb5368ab5c31cc9ff07634becb72c344ee&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360,"size":171357733,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309942","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.torrent&xt=urn:btih:aedfa9479ea04a175eee0b0bd0bda64076308746&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720,"size":497100839,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309943","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.torrent&xt=urn:btih:71971668f82a3b24ac71bc3a982848dd8dc5a5f5&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4&ws=https%3A%2F%2Fpeertube.live%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240,"size":113038439,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309944","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.torrent&xt=urn:btih:c42aa6c95efb28d9f114ebd98537f7b00fa72246&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Fpeertube.iselfhost.com%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4&ws=https%3A%2F%2Ftube.privacytools.io%2Fstatic%2Fredundancy%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240.mp4","height":240},{"type":"Link","mediaType":"application/x-mpegURL","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/master.m3u8","tag":[{"type":"Infohash","name":"f7428214539626e062f300f2ca4cf9154575144e"},{"type":"Infohash","name":"46e236dffb1ea6b9123a5396cbe88e97dd94cc6c"},{"type":"Infohash","name":"11f1045830b5d786c788f2594d19f128764e7d87"},{"type":"Infohash","name":"4327ad3e0d84de100130a27e9ab6fe40c4284f0e"},{"type":"Infohash","name":"41e2eee8e7b23a63c23a77c40a46de11492a4831"},{"type":"Link","name":"sha256","mediaType":"application/json","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/segments-sha256.json"},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080,"size":1156777472,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309940","height":1080,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent","height":1080},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-hls.torrent&xt=urn:btih:0204d780ebfab0d5d9d3476a038e812ad792deeb&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-1080-fragmented.mp4","height":1080},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480,"size":249562889,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309945","height":480,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent","height":480},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-hls.torrent&xt=urn:btih:5d14f38ded29de629668fe1cfc61a75f4cce2628&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-480-fragmented.mp4","height":480},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360,"size":170836415,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309946","height":360,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent","height":360},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-hls.torrent&xt=urn:btih:30125488789080ad405ebcee6c214945f31b8f30&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-360-fragmented.mp4","height":360},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720,"size":496533741,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309947","height":720,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent","height":720},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-hls.torrent&xt=urn:btih:8ed1e8bccde709901c26e315fc8f53bfd26d1ba6&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-720-fragmented.mp4","height":720},{"type":"Link","mediaType":"video/mp4","href":"https://framatube.org/static/streaming-playlists/hls/6050732a-8a7a-43d4-a6cd-809525a1d206/6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240,"size":112529249,"fps":25},{"type":"Link","rel":["metadata","video/mp4"],"mediaType":"application/json","href":"https://framatube.org/api/v1/videos/6050732a-8a7a-43d4-a6cd-809525a1d206/metadata/1309948","height":240,"fps":25},{"type":"Link","mediaType":"application/x-bittorrent","href":"https://framatube.org/static/torrents/6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent","height":240},{"type":"Link","mediaType":"application/x-bittorrent;x-scheme-handler/magnet","href":"magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-hls.torrent&xt=urn:btih:8b452bf4e70b9078d4e74ca8b5523cc9dc70d10a&dn=D%C3%A9framasoftisons+Internet+%5BFramasoft%5D&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F6050732a-8a7a-43d4-a6cd-809525a1d206%2F6050732a-8a7a-43d4-a6cd-809525a1d206-240-fragmented.mp4","height":240}]}],"likes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/likes","dislikes":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/dislikes","shares":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/announces","comments":"https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206/comments","attributedTo":[{"type":"Person","id":"https://framatube.org/accounts/framasoft"},{"type":"Group","id":"https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8"}],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://framatube.org/accounts/framasoft/followers"],"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017"},{"pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","Infohash":"pt:Infohash","Playlist":"pt:Playlist","PlaylistElement":"pt:PlaylistElement","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"},"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}]}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json b/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json
new file mode 100644
index 000000000..1c3f779b3
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json
@@ -0,0 +1 @@
+{"type":"Person","id":"https://framatube.org/accounts/framasoft","following":"https://framatube.org/accounts/framasoft/following","followers":"https://framatube.org/accounts/framasoft/followers","playlists":"https://framatube.org/accounts/framasoft/playlists","inbox":"https://framatube.org/accounts/framasoft/inbox","outbox":"https://framatube.org/accounts/framasoft/outbox","preferredUsername":"framasoft","url":"https://framatube.org/accounts/framasoft","name":"Framasoft","endpoints":{"sharedInbox":"https://framatube.org/inbox"},"publicKey":{"id":"https://framatube.org/accounts/framasoft#main-key","owner":"https://framatube.org/accounts/framasoft","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuRh3frgIg866D0y0FThp\nSUkJImMcHGkUvpYQYv2iUgarZZtEbwT8PfQf0bJazy+cP8KqQmMDf5PBhT7dfdny\nf/GKGMw9Olc+QISeKDj3sqZ3Csrm4KV4avMGCfth6eSU7LozojeSGCXdUFz/8UgE\nfhV4mJjEX/FbwRYoKlagv5rY9mkX5XomzZU+z9j6ZVXyofwOwJvmI1hq0SYDv2bc\neB/RgIh/H0nyMtF8o+0CT42FNEET9j9m1BKOBtPzwZHmitKRkEmui5cK256s1laB\nT61KHpcD9gQKkQ+I3sFEzCBUJYfVo6fUe+GehBZuAfq4qDhd15SfE4K9veDscDFI\nTwIDAQAB\n-----END PUBLIC KEY-----"},"icon":{"type":"Image","mediaType":"image/png","url":"https://framatube.org/lazy-static/avatars/f73876f5-1d45-4f8a-942a-d3d5d5ac5dc1.png"},"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"RsaSignature2017":"https://w3id.org/security#RsaSignature2017","pt":"https://joinpeertube.org/ns#","sc":"http://schema.org#","Hashtag":"as:Hashtag","uuid":"sc:identifier","category":"sc:category","licence":"sc:license","subtitleLanguage":"sc:subtitleLanguage","sensitive":"as:sensitive","language":"sc:inLanguage","expires":"sc:expires","CacheFile":"pt:CacheFile","Infohash":"pt:Infohash","originallyPublishedAt":"sc:datePublished","views":{"@type":"sc:Number","@id":"pt:views"},"state":{"@type":"sc:Number","@id":"pt:state"},"size":{"@type":"sc:Number","@id":"pt:size"},"fps":{"@type":"sc:Number","@id":"pt:fps"},"startTimestamp":{"@type":"sc:Number","@id":"pt:startTimestamp"},"stopTimestamp":{"@type":"sc:Number","@id":"pt:stopTimestamp"},"position":{"@type":"sc:Number","@id":"pt:position"},"commentsEnabled":{"@type":"sc:Boolean","@id":"pt:commentsEnabled"},"downloadEnabled":{"@type":"sc:Boolean","@id":"pt:downloadEnabled"},"waitTranscoding":{"@type":"sc:Boolean","@id":"pt:waitTranscoding"},"support":{"@type":"sc:Text","@id":"pt:support"}},{"likes":{"@id":"as:likes","@type":"@id"},"dislikes":{"@id":"as:dislikes","@type":"@id"},"playlists":{"@id":"pt:playlists","@type":"@id"},"shares":{"@id":"as:shares","@type":"@id"},"comments":{"@id":"as:comments","@type":"@id"}}],"summary":null}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com.html b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html
new file mode 100644
index 000000000..880273d74
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html
@@ -0,0 +1,301 @@
+
+
+
+ Osada
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Welcome to Osada
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 6add3f7eb..13e82ab2a 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -324,6 +324,44 @@ test "it disables notifications from people who are invisible" do
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
refute Notification.create_notification(status, user)
end
+
+ test "it doesn't create notifications if content matches with an irreversible filter" do
+ user = insert(:user)
+ subscriber = insert(:user)
+
+ User.subscribe(subscriber, user)
+ insert(:filter, user: subscriber, phrase: "cofe", hide: true)
+
+ {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"})
+
+ assert {:ok, []} == Notification.create_notifications(status)
+ end
+
+ test "it creates notifications if content matches with a not irreversible filter" do
+ user = insert(:user)
+ subscriber = insert(:user)
+
+ User.subscribe(subscriber, user)
+ insert(:filter, user: subscriber, phrase: "cofe", hide: false)
+
+ {:ok, status} = CommonAPI.post(user, %{status: "got cofe?"})
+ {:ok, [notification]} = Notification.create_notifications(status)
+
+ assert notification
+ end
+
+ test "it creates notifications when someone likes user's status with a filtered word" do
+ user = insert(:user)
+ other_user = insert(:user)
+ insert(:filter, user: user, phrase: "tesla", hide: true)
+
+ {:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"})
+ {:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id)
+
+ {:ok, [notification]} = Notification.create_notifications(activity_two)
+
+ assert notification
+ end
end
describe "follow / follow_request notifications" do
@@ -990,8 +1028,13 @@ test "move activity generates a notification" do
end
describe "for_user" do
- test "it returns notifications for muted user without notifications" do
+ setup do
user = insert(:user)
+
+ {:ok, %{user: user}}
+ end
+
+ test "it returns notifications for muted user without notifications", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted, false)
@@ -1002,8 +1045,7 @@ test "it returns notifications for muted user without notifications" do
assert notification.activity.object
end
- test "it doesn't return notifications for muted user with notifications" do
- user = insert(:user)
+ test "it doesn't return notifications for muted user with notifications", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted)
@@ -1012,8 +1054,7 @@ test "it doesn't return notifications for muted user with notifications" do
assert Notification.for_user(user) == []
end
- test "it doesn't return notifications for blocked user" do
- user = insert(:user)
+ test "it doesn't return notifications for blocked user", %{user: user} do
blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
@@ -1022,8 +1063,7 @@ test "it doesn't return notifications for blocked user" do
assert Notification.for_user(user) == []
end
- test "it doesn't return notifications for domain-blocked non-followed user" do
- user = insert(:user)
+ test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
@@ -1044,8 +1084,7 @@ test "it returns notifications for domain-blocked but followed user" do
assert length(Notification.for_user(user)) == 1
end
- test "it doesn't return notifications for muted thread" do
- user = insert(:user)
+ test "it doesn't return notifications for muted thread", %{user: user} do
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"})
@@ -1054,8 +1093,7 @@ test "it doesn't return notifications for muted thread" do
assert Notification.for_user(user) == []
end
- test "it returns notifications from a muted user when with_muted is set" do
- user = insert(:user)
+ test "it returns notifications from a muted user when with_muted is set", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted)
@@ -1064,8 +1102,9 @@ test "it returns notifications from a muted user when with_muted is set" do
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
- test "it doesn't return notifications from a blocked user when with_muted is set" do
- user = insert(:user)
+ test "it doesn't return notifications from a blocked user when with_muted is set", %{
+ user: user
+ } do
blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
@@ -1075,8 +1114,8 @@ test "it doesn't return notifications from a blocked user when with_muted is set
end
test "when with_muted is set, " <>
- "it doesn't return notifications from a domain-blocked non-followed user" do
- user = insert(:user)
+ "it doesn't return notifications from a domain-blocked non-followed user",
+ %{user: user} do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
@@ -1085,8 +1124,7 @@ test "when with_muted is set, " <>
assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
- test "it returns notifications from muted threads when with_muted is set" do
- user = insert(:user)
+ test "it returns notifications from muted threads when with_muted is set", %{user: user} do
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"})
@@ -1094,5 +1132,33 @@ test "it returns notifications from muted threads when with_muted is set" do
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
+
+ test "it doesn't return notifications about mentions with filtered word", %{user: user} do
+ insert(:filter, user: user, phrase: "cofe", hide: true)
+ another_user = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"})
+
+ assert Enum.empty?(Notification.for_user(user))
+ end
+
+ test "it returns notifications about mentions with not hidden filtered word", %{user: user} do
+ insert(:filter, user: user, phrase: "test", hide: false)
+ another_user = insert(:user)
+
+ {:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"})
+
+ assert length(Notification.for_user(user)) == 1
+ end
+
+ test "it returns notifications about favorites with filtered word", %{user: user} do
+ insert(:filter, user: user, phrase: "cofe", hide: true)
+ another_user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"})
+ {:ok, _} = CommonAPI.favorite(another_user, activity.id)
+
+ assert length(Notification.for_user(user)) == 1
+ end
end
end
diff --git a/test/support/cluster.ex b/test/support/cluster.ex
index deb37f361..524194cf4 100644
--- a/test/support/cluster.ex
+++ b/test/support/cluster.ex
@@ -97,7 +97,7 @@ def spawn_cluster(node_configs) do
silence_logger_warnings(fn ->
node_configs
|> Enum.map(&Task.async(fn -> start_slave(&1) end))
- |> Enum.map(&Task.await(&1, 60_000))
+ |> Enum.map(&Task.await(&1, 90_000))
end)
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index af580021c..635d83650 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -428,4 +428,12 @@ def mfa_token_factory do
user: build(:user)
}
end
+
+ def filter_factory do
+ %Pleroma.Filter{
+ user: build(:user),
+ filter_id: sequence(:filter_id, & &1),
+ phrase: "cofe"
+ }
+ end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 3d5128835..19a202654 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -308,6 +308,22 @@ def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
}}
end
+ def get("https://framatube.org/accounts/framasoft", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/https___framatube.org_accounts_framasoft.json")
+ }}
+ end
+
+ def get("https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/framatube.org-video.json")
+ }}
+ end
+
def get("https://peertube.social/accounts/craigmaloney", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1326,6 +1342,18 @@ def get("https://relay.mastodon.host/actor", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}}
end
+ def get("http://localhost:4001/", _, "", Accept: "text/html") do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}}
+ end
+
+ def get("https://osada.macgirvin.com/", _, "", Accept: "text/html") do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com.html")
+ }}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs
index a8ba0658d..79ab72002 100644
--- a/test/tasks/relay_test.exs
+++ b/test/tasks/relay_test.exs
@@ -10,6 +10,8 @@ defmodule Mix.Tasks.Pleroma.RelayTest do
alias Pleroma.Web.ActivityPub.Utils
use Pleroma.DataCase
+ import Pleroma.Factory
+
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -46,7 +48,8 @@ test "relay is followed" do
describe "running unfollow" do
test "relay is unfollowed" do
- target_instance = "http://mastodon.example.org/users/admin"
+ user = insert(:user)
+ target_instance = user.ap_id
Mix.Tasks.Pleroma.Relay.run(["follow", target_instance])
@@ -71,7 +74,7 @@ test "relay is unfollowed" do
assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id
- assert undo_activity.data["object"] == cancelled_activity.data
+ assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"]
refute "#{target_instance}/followers" in User.following(local_user)
end
end
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index 9220d23fc..ce43a9cc7 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -110,7 +110,23 @@ test "user is deleted" do
test "a remote user's create activity is deleted when the object has been pruned" do
user = insert(:user)
+ user2 = insert(:user)
+
{:ok, post} = CommonAPI.post(user, %{status: "uguu"})
+ {:ok, post2} = CommonAPI.post(user2, %{status: "test"})
+ obj = Object.normalize(post2)
+
+ {:ok, like_object, meta} = Pleroma.Web.ActivityPub.Builder.like(user, obj)
+
+ {:ok, like_activity, _meta} =
+ Pleroma.Web.ActivityPub.Pipeline.common_pipeline(
+ like_object,
+ Keyword.put(meta, :local, true)
+ )
+
+ like_activity.data["object"]
+ |> Pleroma.Object.get_by_ap_id()
+ |> Repo.delete()
clear_config([:instance, :federating], true)
@@ -127,6 +143,7 @@ test "a remote user's create activity is deleted when the object has been pruned
assert %{deactivated: true} = User.get_by_nickname(user.nickname)
assert called(Pleroma.Web.Federator.publish(:_))
+ refute Pleroma.Repo.get(Pleroma.Activity, like_activity.id)
end
refute Activity.get_by_id(post.id)
@@ -464,17 +481,17 @@ test "it returns users matching" do
moot = insert(:user, nickname: "moot")
kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon")
- {:ok, user} = User.follow(user, kawen)
+ {:ok, user} = User.follow(user, moon)
assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id)
- res = User.search("moo") |> Enum.map(& &1.id)
- assert moon.id in res
- assert moot.id in res
- assert kawen.id in res
- assert [moon.id, kawen.id] == User.Search.search("moon fediverse") |> Enum.map(& &1.id)
- assert [kawen.id, moon.id] ==
- User.Search.search("moon fediverse", for_user: user) |> Enum.map(& &1.id)
+ res = User.search("moo") |> Enum.map(& &1.id)
+ assert Enum.sort([moon.id, moot.id, kawen.id]) == Enum.sort(res)
+
+ assert [kawen.id, moon.id] == User.Search.search("expert fediverse") |> Enum.map(& &1.id)
+
+ assert [moon.id, kawen.id] ==
+ User.Search.search("expert fediverse", for_user: user) |> Enum.map(& &1.id)
end
end
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 2abf0edec..b06b54487 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -107,6 +107,19 @@ test "it returns error" do
describe "Storing a file with the Local uploader" do
setup [:ensure_local_uploader]
+ test "does not allow descriptions longer than the post limit" do
+ clear_config([:instance, :description_limit], 2)
+ File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image_tmp.jpg"),
+ filename: "image.jpg"
+ }
+
+ {:error, :description_too_long} = Upload.store(file, description: "123")
+ end
+
test "returns a media url" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index f030523d3..559ba5966 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -46,30 +46,49 @@ test "accepts offset parameter" do
assert length(User.search("john", limit: 3, offset: 3)) == 2
end
- test "finds a user by full or partial nickname" do
+ defp clear_virtual_fields(user) do
+ Map.merge(user, %{search_rank: nil, search_type: nil})
+ end
+
+ test "finds a user by full nickname or its leading fragment" do
user = insert(:user, %{nickname: "john"})
Enum.each(["john", "jo", "j"], fn query ->
assert user ==
User.search(query)
|> List.first()
- |> Map.put(:search_rank, nil)
- |> Map.put(:search_type, nil)
+ |> clear_virtual_fields()
end)
end
- test "finds a user by full or partial name" do
+ test "finds a user by full name or leading fragment(s) of its words" do
user = insert(:user, %{name: "John Doe"})
Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
assert user ==
User.search(query)
|> List.first()
- |> Map.put(:search_rank, nil)
- |> Map.put(:search_type, nil)
+ |> clear_virtual_fields()
end)
end
+ test "matches by leading fragment of user domain" do
+ user = insert(:user, %{nickname: "arandom@dude.com"})
+ insert(:user, %{nickname: "iamthedude"})
+
+ assert [user.id] == User.search("dud") |> Enum.map(& &1.id)
+ end
+
+ test "ranks full nickname match higher than full name match" do
+ nicknamed_user = insert(:user, %{nickname: "hj@shigusegubu.club"})
+ named_user = insert(:user, %{nickname: "xyz@sample.com", name: "HJ"})
+
+ results = User.search("hj")
+
+ assert [nicknamed_user.id, named_user.id] == Enum.map(results, & &1.id)
+ assert Enum.at(results, 0).search_rank > Enum.at(results, 1).search_rank
+ end
+
test "finds users, considering density of matched tokens" do
u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index ef69f3d91..b988e4437 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -514,6 +514,33 @@ test "retrieves activities that have a given context" do
activities = ActivityPub.fetch_activities_for_context("2hu", %{blocking_user: user})
assert activities == [activity_two, activity]
end
+
+ test "doesn't return activities with filtered words" do
+ user = insert(:user)
+ user_two = insert(:user)
+ insert(:filter, user: user, phrase: "test", hide: true)
+
+ {:ok, %{id: id1, data: %{"context" => context}}} = CommonAPI.post(user, %{status: "1"})
+
+ {:ok, %{id: id2}} = CommonAPI.post(user_two, %{status: "2", in_reply_to_status_id: id1})
+
+ {:ok, %{id: id3} = user_activity} =
+ CommonAPI.post(user, %{status: "3 test?", in_reply_to_status_id: id2})
+
+ {:ok, %{id: id4} = filtered_activity} =
+ CommonAPI.post(user_two, %{status: "4 test!", in_reply_to_status_id: id3})
+
+ {:ok, _} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
+
+ activities =
+ context
+ |> ActivityPub.fetch_activities_for_context(%{user: user})
+ |> Enum.map(& &1.id)
+
+ assert length(activities) == 4
+ assert user_activity.id in activities
+ refute filtered_activity.id in activities
+ end
end
test "doesn't return blocked activities" do
@@ -649,7 +676,7 @@ test "doesn't return activities from blocked domains" do
refute activity in activities
followed_user = insert(:user)
- ActivityPub.follow(user, followed_user)
+ CommonAPI.follow(user, followed_user)
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user)
activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
@@ -792,6 +819,75 @@ test "excludes reblogs on request" do
assert activity == expected_activity
end
+ describe "irreversible filters" do
+ setup do
+ user = insert(:user)
+ user_two = insert(:user)
+
+ insert(:filter, user: user_two, phrase: "cofe", hide: true)
+ insert(:filter, user: user_two, phrase: "ok boomer", hide: true)
+ insert(:filter, user: user_two, phrase: "test", hide: false)
+
+ params = %{
+ type: ["Create", "Announce"],
+ user: user_two
+ }
+
+ {:ok, %{user: user, user_two: user_two, params: params}}
+ end
+
+ test "it returns statuses if they don't contain exact filter words", %{
+ user: user,
+ params: params
+ } do
+ {:ok, _} = CommonAPI.post(user, %{status: "hey"})
+ {:ok, _} = CommonAPI.post(user, %{status: "got cofefe?"})
+ {:ok, _} = CommonAPI.post(user, %{status: "I am not a boomer"})
+ {:ok, _} = CommonAPI.post(user, %{status: "ok boomers"})
+ {:ok, _} = CommonAPI.post(user, %{status: "ccofee is not a word"})
+ {:ok, _} = CommonAPI.post(user, %{status: "this is a test"})
+
+ activities = ActivityPub.fetch_activities([], params)
+
+ assert Enum.count(activities) == 6
+ end
+
+ test "it does not filter user's own statuses", %{user_two: user_two, params: params} do
+ {:ok, _} = CommonAPI.post(user_two, %{status: "Give me some cofe!"})
+ {:ok, _} = CommonAPI.post(user_two, %{status: "ok boomer"})
+
+ activities = ActivityPub.fetch_activities([], params)
+
+ assert Enum.count(activities) == 2
+ end
+
+ test "it excludes statuses with filter words", %{user: user, params: params} do
+ {:ok, _} = CommonAPI.post(user, %{status: "Give me some cofe!"})
+ {:ok, _} = CommonAPI.post(user, %{status: "ok boomer"})
+ {:ok, _} = CommonAPI.post(user, %{status: "is it a cOfE?"})
+ {:ok, _} = CommonAPI.post(user, %{status: "cofe is all I need"})
+ {:ok, _} = CommonAPI.post(user, %{status: "— ok BOOMER\n"})
+
+ activities = ActivityPub.fetch_activities([], params)
+
+ assert Enum.empty?(activities)
+ end
+
+ test "it returns all statuses if user does not have any filters" do
+ another_user = insert(:user)
+ {:ok, _} = CommonAPI.post(another_user, %{status: "got cofe?"})
+ {:ok, _} = CommonAPI.post(another_user, %{status: "test!"})
+
+ activities =
+ ActivityPub.fetch_activities([], %{
+ type: ["Create", "Announce"],
+ user: another_user
+ })
+
+ assert Enum.count(activities) == 2
+ end
+ end
+
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
@@ -924,24 +1020,12 @@ test "fetches the latest Follow activity" do
end
end
- describe "following / unfollowing" do
- test "it reverts follow activity" do
- follower = insert(:user)
- followed = insert(:user)
-
- with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
- assert {:error, :reverted} = ActivityPub.follow(follower, followed)
- end
-
- assert Repo.aggregate(Activity, :count, :id) == 0
- assert Repo.aggregate(Object, :count, :id) == 0
- end
-
+ describe "unfollowing" do
test "it reverts unfollow activity" do
follower = insert(:user)
followed = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
@@ -954,21 +1038,11 @@ test "it reverts unfollow activity" do
assert activity.data["object"] == followed.ap_id
end
- test "creates a follow activity" do
- follower = insert(:user)
- followed = insert(:user)
-
- {:ok, activity} = ActivityPub.follow(follower, followed)
- assert activity.data["type"] == "Follow"
- assert activity.data["actor"] == follower.ap_id
- assert activity.data["object"] == followed.ap_id
- end
-
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
@@ -985,7 +1059,7 @@ test "creates an undo activity for a pending follow request" do
follower = insert(:user)
followed = insert(:user, %{locked: true})
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
deleted file mode 100644
index c1a872297..000000000
--- a/test/web/activity_pub/object_validator_test.exs
+++ /dev/null
@@ -1,695 +0,0 @@
-defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
- use Pleroma.DataCase
-
- alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Builder
- alias Pleroma.Web.ActivityPub.ObjectValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
- alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
- alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.CommonAPI
-
- import Pleroma.Factory
-
- describe "attachments" do
- test "works with honkerific attachments" do
- attachment = %{
- "mediaType" => "",
- "name" => "",
- "summary" => "298p3RG7j27tfsZ9RQ.jpg",
- "type" => "Document",
- "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
- }
-
- assert {:ok, attachment} =
- AttachmentValidator.cast_and_validate(attachment)
- |> Ecto.Changeset.apply_action(:insert)
-
- assert attachment.mediaType == "application/octet-stream"
- end
-
- test "it turns mastodon attachments into our attachments" do
- attachment = %{
- "url" =>
- "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
- "type" => "Document",
- "name" => nil,
- "mediaType" => "image/jpeg"
- }
-
- {:ok, attachment} =
- AttachmentValidator.cast_and_validate(attachment)
- |> Ecto.Changeset.apply_action(:insert)
-
- assert [
- %{
- href:
- "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
- type: "Link",
- mediaType: "image/jpeg"
- }
- ] = attachment.url
-
- assert attachment.mediaType == "image/jpeg"
- end
-
- test "it handles our own uploads" do
- user = insert(:user)
-
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
-
- {:ok, attachment} =
- attachment.data
- |> AttachmentValidator.cast_and_validate()
- |> Ecto.Changeset.apply_action(:insert)
-
- assert attachment.mediaType == "image/jpeg"
- end
- end
-
- describe "chat message create activities" do
- test "it is invalid if the object already exists" do
- user = insert(:user)
- recipient = insert(:user)
- {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
- object = Object.normalize(activity, false)
-
- {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
-
- {:error, cng} = ObjectValidator.validate(create_data, [])
-
- assert {:object, {"The object to create already exists", []}} in cng.errors
- end
-
- test "it is invalid if the object data has a different `to` or `actor` field" do
- user = insert(:user)
- recipient = insert(:user)
- {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
-
- {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
-
- {:error, cng} = ObjectValidator.validate(create_data, [])
-
- assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
- assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
- end
- end
-
- describe "chat messages" do
- setup do
- clear_config([:instance, :remote_limit])
- user = insert(:user)
- recipient = insert(:user, local: false)
-
- {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
-
- %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
- end
-
- test "let's through some basic html", %{user: user, recipient: recipient} do
- {:ok, valid_chat_message, _} =
- Builder.chat_message(
- user,
- recipient.ap_id,
- "hey example "
- )
-
- assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
-
- assert object["content"] ==
- "hey example alert('uguu')"
- end
-
- test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
- assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
-
- assert Map.put(valid_chat_message, "attachment", nil) == object
- end
-
- test "validates for a basic object with an attachment", %{
- valid_chat_message: valid_chat_message,
- user: user
- } do
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
-
- valid_chat_message =
- valid_chat_message
- |> Map.put("attachment", attachment.data)
-
- assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
-
- assert object["attachment"]
- end
-
- test "validates for a basic object with an attachment in an array", %{
- valid_chat_message: valid_chat_message,
- user: user
- } do
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
-
- valid_chat_message =
- valid_chat_message
- |> Map.put("attachment", [attachment.data])
-
- assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
-
- assert object["attachment"]
- end
-
- test "validates for a basic object with an attachment but without content", %{
- valid_chat_message: valid_chat_message,
- user: user
- } do
- file = %Plug.Upload{
- content_type: "image/jpg",
- path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
- }
-
- {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
-
- valid_chat_message =
- valid_chat_message
- |> Map.put("attachment", attachment.data)
- |> Map.delete("content")
-
- assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
-
- assert object["attachment"]
- end
-
- test "does not validate if the message has no content", %{
- valid_chat_message: valid_chat_message
- } do
- contentless =
- valid_chat_message
- |> Map.delete("content")
-
- refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
- end
-
- test "does not validate if the message is longer than the remote_limit", %{
- valid_chat_message: valid_chat_message
- } do
- Pleroma.Config.put([:instance, :remote_limit], 2)
- refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
- end
-
- test "does not validate if the recipient is blocking the actor", %{
- valid_chat_message: valid_chat_message,
- user: user,
- recipient: recipient
- } do
- Pleroma.User.block(recipient, user)
- refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
- end
-
- test "does not validate if the recipient is not accepting chat messages", %{
- valid_chat_message: valid_chat_message,
- recipient: recipient
- } do
- recipient
- |> Ecto.Changeset.change(%{accepts_chat_messages: false})
- |> Pleroma.Repo.update!()
-
- refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
- end
-
- test "does not validate if the actor or the recipient is not in our system", %{
- valid_chat_message: valid_chat_message
- } do
- chat_message =
- valid_chat_message
- |> Map.put("actor", "https://raymoo.com/raymoo")
-
- {:error, _} = ObjectValidator.validate(chat_message, [])
-
- chat_message =
- valid_chat_message
- |> Map.put("to", ["https://raymoo.com/raymoo"])
-
- {:error, _} = ObjectValidator.validate(chat_message, [])
- end
-
- test "does not validate for a message with multiple recipients", %{
- valid_chat_message: valid_chat_message,
- user: user,
- recipient: recipient
- } do
- chat_message =
- valid_chat_message
- |> Map.put("to", [user.ap_id, recipient.ap_id])
-
- assert {:error, _} = ObjectValidator.validate(chat_message, [])
- end
-
- test "does not validate if it doesn't concern local users" do
- user = insert(:user, local: false)
- recipient = insert(:user, local: false)
-
- {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
- assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
- end
- end
-
- describe "EmojiReacts" do
- setup do
- user = insert(:user)
- {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
-
- object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
-
- {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
-
- %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
- end
-
- test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
- assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
- end
-
- test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
- without_content =
- valid_emoji_react
- |> Map.delete("content")
-
- {:error, cng} = ObjectValidator.validate(without_content, [])
-
- refute cng.valid?
- assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
- end
-
- test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
- without_emoji_content =
- valid_emoji_react
- |> Map.put("content", "x")
-
- {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
-
- refute cng.valid?
-
- assert {:content, {"must be a single character emoji", []}} in cng.errors
- end
- end
-
- describe "Undos" do
- setup do
- user = insert(:user)
- {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
- {:ok, like} = CommonAPI.favorite(user, post_activity.id)
- {:ok, valid_like_undo, []} = Builder.undo(user, like)
-
- %{user: user, like: like, valid_like_undo: valid_like_undo}
- end
-
- test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
- assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
- end
-
- test "it does not validate if the actor of the undo is not the actor of the object", %{
- valid_like_undo: valid_like_undo
- } do
- other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
-
- bad_actor =
- valid_like_undo
- |> Map.put("actor", other_user.ap_id)
-
- {:error, cng} = ObjectValidator.validate(bad_actor, [])
-
- assert {:actor, {"not the same as object actor", []}} in cng.errors
- end
-
- test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
- missing_object =
- valid_like_undo
- |> Map.put("object", "https://gensokyo.2hu/objects/1")
-
- {:error, cng} = ObjectValidator.validate(missing_object, [])
-
- assert {:object, {"can't find object", []}} in cng.errors
- assert length(cng.errors) == 1
- end
- end
-
- describe "deletes" do
- setup do
- user = insert(:user)
- {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"})
-
- {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
- {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
-
- %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete}
- end
-
- test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
- {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, [])
-
- assert valid_post_delete["deleted_activity_id"]
- end
-
- test "it is invalid if the object isn't in a list of certain types", %{
- valid_post_delete: valid_post_delete
- } do
- object = Object.get_by_ap_id(valid_post_delete["object"])
-
- data =
- object.data
- |> Map.put("type", "Like")
-
- {:ok, _object} =
- object
- |> Ecto.Changeset.change(%{data: data})
- |> Object.update_and_set_cache()
-
- {:error, cng} = ObjectValidator.validate(valid_post_delete, [])
- assert {:object, {"object not in allowed types", []}} in cng.errors
- end
-
- test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do
- assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, []))
- end
-
- test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
- no_id =
- valid_post_delete
- |> Map.delete("id")
-
- {:error, cng} = ObjectValidator.validate(no_id, [])
-
- assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
- end
-
- test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
- missing_object =
- valid_post_delete
- |> Map.put("object", "http://does.not/exist")
-
- {:error, cng} = ObjectValidator.validate(missing_object, [])
-
- assert {:object, {"can't find object", []}} in cng.errors
- end
-
- test "it's invalid if the actor of the object and the actor of delete are from different domains",
- %{valid_post_delete: valid_post_delete} do
- valid_user = insert(:user)
-
- valid_other_actor =
- valid_post_delete
- |> Map.put("actor", valid_user.ap_id)
-
- assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
-
- invalid_other_actor =
- valid_post_delete
- |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
-
- {:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
-
- assert {:actor, {"is not allowed to delete object", []}} in cng.errors
- end
-
- test "it's valid if the actor of the object is a local superuser",
- %{valid_post_delete: valid_post_delete} do
- user =
- insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
-
- valid_other_actor =
- valid_post_delete
- |> Map.put("actor", user.ap_id)
-
- {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
- assert meta[:do_not_federate]
- end
- end
-
- describe "likes" do
- setup do
- user = insert(:user)
- {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
-
- valid_like = %{
- "to" => [user.ap_id],
- "cc" => [],
- "type" => "Like",
- "id" => Utils.generate_activity_id(),
- "object" => post_activity.data["object"],
- "actor" => user.ap_id,
- "context" => "a context"
- }
-
- %{valid_like: valid_like, user: user, post_activity: post_activity}
- end
-
- test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
- {:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
-
- assert "id" in Map.keys(object)
- end
-
- test "is valid for a valid object", %{valid_like: valid_like} do
- assert LikeValidator.cast_and_validate(valid_like).valid?
- end
-
- test "sets the 'to' field to the object actor if no recipients are given", %{
- valid_like: valid_like,
- user: user
- } do
- without_recipients =
- valid_like
- |> Map.delete("to")
-
- {:ok, object, _meta} = ObjectValidator.validate(without_recipients, [])
-
- assert object["to"] == [user.ap_id]
- end
-
- test "sets the context field to the context of the object if no context is given", %{
- valid_like: valid_like,
- post_activity: post_activity
- } do
- without_context =
- valid_like
- |> Map.delete("context")
-
- {:ok, object, _meta} = ObjectValidator.validate(without_context, [])
-
- assert object["context"] == post_activity.data["context"]
- end
-
- test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
- without_actor = Map.delete(valid_like, "actor")
-
- refute LikeValidator.cast_and_validate(without_actor).valid?
-
- with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
-
- refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
- end
-
- test "it errors when the object is missing or not known", %{valid_like: valid_like} do
- without_object = Map.delete(valid_like, "object")
-
- refute LikeValidator.cast_and_validate(without_object).valid?
-
- with_invalid_object = Map.put(valid_like, "object", "invalidobject")
-
- refute LikeValidator.cast_and_validate(with_invalid_object).valid?
- end
-
- test "it errors when the actor has already like the object", %{
- valid_like: valid_like,
- user: user,
- post_activity: post_activity
- } do
- _like = CommonAPI.favorite(user, post_activity.id)
-
- refute LikeValidator.cast_and_validate(valid_like).valid?
- end
-
- test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
- wrapped_like =
- valid_like
- |> Map.put("actor", %{"id" => valid_like["actor"]})
- |> Map.put("object", %{"id" => valid_like["object"]})
-
- validated = LikeValidator.cast_and_validate(wrapped_like)
-
- assert validated.valid?
-
- assert {:actor, valid_like["actor"]} in validated.changes
- assert {:object, valid_like["object"]} in validated.changes
- end
- end
-
- describe "announces" do
- setup do
- user = insert(:user)
- announcer = insert(:user)
- {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
-
- object = Object.normalize(post_activity, false)
- {:ok, valid_announce, []} = Builder.announce(announcer, object)
-
- %{
- valid_announce: valid_announce,
- user: user,
- post_activity: post_activity,
- announcer: announcer
- }
- end
-
- test "returns ok for a valid announce", %{valid_announce: valid_announce} do
- assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, [])
- end
-
- test "returns an error if the object can't be found", %{valid_announce: valid_announce} do
- without_object =
- valid_announce
- |> Map.delete("object")
-
- {:error, cng} = ObjectValidator.validate(without_object, [])
-
- assert {:object, {"can't be blank", [validation: :required]}} in cng.errors
-
- nonexisting_object =
- valid_announce
- |> Map.put("object", "https://gensokyo.2hu/objects/99999999")
-
- {:error, cng} = ObjectValidator.validate(nonexisting_object, [])
-
- assert {:object, {"can't find object", []}} in cng.errors
- end
-
- test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do
- nonexisting_actor =
- valid_announce
- |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
-
- {:error, cng} = ObjectValidator.validate(nonexisting_actor, [])
-
- assert {:actor, {"can't find user", []}} in cng.errors
- end
-
- test "returns an error if the actor already announced the object", %{
- valid_announce: valid_announce,
- announcer: announcer,
- post_activity: post_activity
- } do
- _announce = CommonAPI.repeat(post_activity.id, announcer)
-
- {:error, cng} = ObjectValidator.validate(valid_announce, [])
-
- assert {:actor, {"already announced this object", []}} in cng.errors
- assert {:object, {"already announced by this actor", []}} in cng.errors
- end
-
- test "returns an error if the actor can't announce the object", %{
- announcer: announcer,
- user: user
- } do
- {:ok, post_activity} =
- CommonAPI.post(user, %{status: "a secret post", visibility: "private"})
-
- object = Object.normalize(post_activity, false)
-
- # Another user can't announce it
- {:ok, announce, []} = Builder.announce(announcer, object, public: false)
-
- {:error, cng} = ObjectValidator.validate(announce, [])
-
- assert {:actor, {"can not announce this object", []}} in cng.errors
-
- # The actor of the object can announce it
- {:ok, announce, []} = Builder.announce(user, object, public: false)
-
- assert {:ok, _, _} = ObjectValidator.validate(announce, [])
-
- # The actor of the object can not announce it publicly
- {:ok, announce, []} = Builder.announce(user, object, public: true)
-
- {:error, cng} = ObjectValidator.validate(announce, [])
-
- assert {:actor, {"can not announce this object publicly", []}} in cng.errors
- end
- end
-
- describe "updates" do
- setup do
- user = insert(:user)
-
- object = %{
- "id" => user.ap_id,
- "name" => "A new name",
- "summary" => "A new bio"
- }
-
- {:ok, valid_update, []} = Builder.update(user, object)
-
- %{user: user, valid_update: valid_update}
- end
-
- test "validates a basic object", %{valid_update: valid_update} do
- assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
- end
-
- test "returns an error if the object can't be updated by the actor", %{
- valid_update: valid_update
- } do
- other_user = insert(:user)
-
- update =
- valid_update
- |> Map.put("actor", other_user.ap_id)
-
- assert {:error, _cng} = ObjectValidator.validate(update, [])
- end
- end
-
- describe "blocks" do
- setup do
- user = insert(:user, local: false)
- blocked = insert(:user)
-
- {:ok, valid_block, []} = Builder.block(user, blocked)
-
- %{user: user, valid_block: valid_block}
- end
-
- test "validates a basic object", %{
- valid_block: valid_block
- } do
- assert {:ok, _block, []} = ObjectValidator.validate(valid_block, [])
- end
-
- test "returns an error if we don't know the blocked user", %{
- valid_block: valid_block
- } do
- block =
- valid_block
- |> Map.put("object", "https://gensokyo.2hu/users/raymoo")
-
- assert {:error, _cng} = ObjectValidator.validate(block, [])
- end
- end
-end
diff --git a/test/web/activity_pub/object_validators/announce_validation_test.exs b/test/web/activity_pub/object_validators/announce_validation_test.exs
new file mode 100644
index 000000000..623342f76
--- /dev/null
+++ b/test/web/activity_pub/object_validators/announce_validation_test.exs
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnouncValidationTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "announces" do
+ setup do
+ user = insert(:user)
+ announcer = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+
+ object = Object.normalize(post_activity, false)
+ {:ok, valid_announce, []} = Builder.announce(announcer, object)
+
+ %{
+ valid_announce: valid_announce,
+ user: user,
+ post_activity: post_activity,
+ announcer: announcer
+ }
+ end
+
+ test "returns ok for a valid announce", %{valid_announce: valid_announce} do
+ assert {:ok, _object, _meta} = ObjectValidator.validate(valid_announce, [])
+ end
+
+ test "returns an error if the object can't be found", %{valid_announce: valid_announce} do
+ without_object =
+ valid_announce
+ |> Map.delete("object")
+
+ {:error, cng} = ObjectValidator.validate(without_object, [])
+
+ assert {:object, {"can't be blank", [validation: :required]}} in cng.errors
+
+ nonexisting_object =
+ valid_announce
+ |> Map.put("object", "https://gensokyo.2hu/objects/99999999")
+
+ {:error, cng} = ObjectValidator.validate(nonexisting_object, [])
+
+ assert {:object, {"can't find object", []}} in cng.errors
+ end
+
+ test "returns an error if we don't have the actor", %{valid_announce: valid_announce} do
+ nonexisting_actor =
+ valid_announce
+ |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+
+ {:error, cng} = ObjectValidator.validate(nonexisting_actor, [])
+
+ assert {:actor, {"can't find user", []}} in cng.errors
+ end
+
+ test "returns an error if the actor already announced the object", %{
+ valid_announce: valid_announce,
+ announcer: announcer,
+ post_activity: post_activity
+ } do
+ _announce = CommonAPI.repeat(post_activity.id, announcer)
+
+ {:error, cng} = ObjectValidator.validate(valid_announce, [])
+
+ assert {:actor, {"already announced this object", []}} in cng.errors
+ assert {:object, {"already announced by this actor", []}} in cng.errors
+ end
+
+ test "returns an error if the actor can't announce the object", %{
+ announcer: announcer,
+ user: user
+ } do
+ {:ok, post_activity} =
+ CommonAPI.post(user, %{status: "a secret post", visibility: "private"})
+
+ object = Object.normalize(post_activity, false)
+
+ # Another user can't announce it
+ {:ok, announce, []} = Builder.announce(announcer, object, public: false)
+
+ {:error, cng} = ObjectValidator.validate(announce, [])
+
+ assert {:actor, {"can not announce this object", []}} in cng.errors
+
+ # The actor of the object can announce it
+ {:ok, announce, []} = Builder.announce(user, object, public: false)
+
+ assert {:ok, _, _} = ObjectValidator.validate(announce, [])
+
+ # The actor of the object can not announce it publicly
+ {:ok, announce, []} = Builder.announce(user, object, public: true)
+
+ {:error, cng} = ObjectValidator.validate(announce, [])
+
+ assert {:actor, {"can not announce this object publicly", []}} in cng.errors
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/attachment_validator_test.exs b/test/web/activity_pub/object_validators/attachment_validator_test.exs
new file mode 100644
index 000000000..558bb3131
--- /dev/null
+++ b/test/web/activity_pub/object_validators/attachment_validator_test.exs
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
+
+ import Pleroma.Factory
+
+ describe "attachments" do
+ test "works with honkerific attachments" do
+ attachment = %{
+ "mediaType" => "",
+ "name" => "",
+ "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+ "type" => "Document",
+ "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+ }
+
+ assert {:ok, attachment} =
+ AttachmentValidator.cast_and_validate(attachment)
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert attachment.mediaType == "application/octet-stream"
+ end
+
+ test "it turns mastodon attachments into our attachments" do
+ attachment = %{
+ "url" =>
+ "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+ "type" => "Document",
+ "name" => nil,
+ "mediaType" => "image/jpeg"
+ }
+
+ {:ok, attachment} =
+ AttachmentValidator.cast_and_validate(attachment)
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert [
+ %{
+ href:
+ "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+ type: "Link",
+ mediaType: "image/jpeg"
+ }
+ ] = attachment.url
+
+ assert attachment.mediaType == "image/jpeg"
+ end
+
+ test "it handles our own uploads" do
+ user = insert(:user)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ {:ok, attachment} =
+ attachment.data
+ |> AttachmentValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert attachment.mediaType == "image/jpeg"
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/block_validation_test.exs b/test/web/activity_pub/object_validators/block_validation_test.exs
new file mode 100644
index 000000000..c08d4b2e8
--- /dev/null
+++ b/test/web/activity_pub/object_validators/block_validation_test.exs
@@ -0,0 +1,39 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidationTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+
+ import Pleroma.Factory
+
+ describe "blocks" do
+ setup do
+ user = insert(:user, local: false)
+ blocked = insert(:user)
+
+ {:ok, valid_block, []} = Builder.block(user, blocked)
+
+ %{user: user, valid_block: valid_block}
+ end
+
+ test "validates a basic object", %{
+ valid_block: valid_block
+ } do
+ assert {:ok, _block, []} = ObjectValidator.validate(valid_block, [])
+ end
+
+ test "returns an error if we don't know the blocked user", %{
+ valid_block: valid_block
+ } do
+ block =
+ valid_block
+ |> Map.put("object", "https://gensokyo.2hu/users/raymoo")
+
+ assert {:error, _cng} = ObjectValidator.validate(block, [])
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/chat_validation_test.exs b/test/web/activity_pub/object_validators/chat_validation_test.exs
new file mode 100644
index 000000000..50bf03515
--- /dev/null
+++ b/test/web/activity_pub/object_validators/chat_validation_test.exs
@@ -0,0 +1,211 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do
+ use Pleroma.DataCase
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "chat message create activities" do
+ test "it is invalid if the object already exists" do
+ user = insert(:user)
+ recipient = insert(:user)
+ {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
+ object = Object.normalize(activity, false)
+
+ {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
+
+ {:error, cng} = ObjectValidator.validate(create_data, [])
+
+ assert {:object, {"The object to create already exists", []}} in cng.errors
+ end
+
+ test "it is invalid if the object data has a different `to` or `actor` field" do
+ user = insert(:user)
+ recipient = insert(:user)
+ {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
+
+ {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
+
+ {:error, cng} = ObjectValidator.validate(create_data, [])
+
+ assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
+ assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
+ end
+ end
+
+ describe "chat messages" do
+ setup do
+ clear_config([:instance, :remote_limit])
+ user = insert(:user)
+ recipient = insert(:user, local: false)
+
+ {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
+
+ %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
+ end
+
+ test "let's through some basic html", %{user: user, recipient: recipient} do
+ {:ok, valid_chat_message, _} =
+ Builder.chat_message(
+ user,
+ recipient.ap_id,
+ "hey example "
+ )
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["content"] ==
+ "hey example alert('uguu')"
+ end
+
+ test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert Map.put(valid_chat_message, "attachment", nil) == object
+ end
+
+ test "validates for a basic object with an attachment", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", attachment.data)
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "validates for a basic object with an attachment in an array", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", [attachment.data])
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "validates for a basic object with an attachment but without content", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", attachment.data)
+ |> Map.delete("content")
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "does not validate if the message has no content", %{
+ valid_chat_message: valid_chat_message
+ } do
+ contentless =
+ valid_chat_message
+ |> Map.delete("content")
+
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
+ end
+
+ test "does not validate if the message is longer than the remote_limit", %{
+ valid_chat_message: valid_chat_message
+ } do
+ Pleroma.Config.put([:instance, :remote_limit], 2)
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+ end
+
+ test "does not validate if the recipient is blocking the actor", %{
+ valid_chat_message: valid_chat_message,
+ user: user,
+ recipient: recipient
+ } do
+ Pleroma.User.block(recipient, user)
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+ end
+
+ test "does not validate if the recipient is not accepting chat messages", %{
+ valid_chat_message: valid_chat_message,
+ recipient: recipient
+ } do
+ recipient
+ |> Ecto.Changeset.change(%{accepts_chat_messages: false})
+ |> Pleroma.Repo.update!()
+
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+ end
+
+ test "does not validate if the actor or the recipient is not in our system", %{
+ valid_chat_message: valid_chat_message
+ } do
+ chat_message =
+ valid_chat_message
+ |> Map.put("actor", "https://raymoo.com/raymoo")
+
+ {:error, _} = ObjectValidator.validate(chat_message, [])
+
+ chat_message =
+ valid_chat_message
+ |> Map.put("to", ["https://raymoo.com/raymoo"])
+
+ {:error, _} = ObjectValidator.validate(chat_message, [])
+ end
+
+ test "does not validate for a message with multiple recipients", %{
+ valid_chat_message: valid_chat_message,
+ user: user,
+ recipient: recipient
+ } do
+ chat_message =
+ valid_chat_message
+ |> Map.put("to", [user.ap_id, recipient.ap_id])
+
+ assert {:error, _} = ObjectValidator.validate(chat_message, [])
+ end
+
+ test "does not validate if it doesn't concern local users" do
+ user = insert(:user, local: false)
+ recipient = insert(:user, local: false)
+
+ {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
+ assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/delete_validation_test.exs b/test/web/activity_pub/object_validators/delete_validation_test.exs
new file mode 100644
index 000000000..42cd18298
--- /dev/null
+++ b/test/web/activity_pub/object_validators/delete_validation_test.exs
@@ -0,0 +1,106 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "deletes" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{status: "cancel me daddy"})
+
+ {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
+ {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
+
+ %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete}
+ end
+
+ test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
+ {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, [])
+
+ assert valid_post_delete["deleted_activity_id"]
+ end
+
+ test "it is invalid if the object isn't in a list of certain types", %{
+ valid_post_delete: valid_post_delete
+ } do
+ object = Object.get_by_ap_id(valid_post_delete["object"])
+
+ data =
+ object.data
+ |> Map.put("type", "Like")
+
+ {:ok, _object} =
+ object
+ |> Ecto.Changeset.change(%{data: data})
+ |> Object.update_and_set_cache()
+
+ {:error, cng} = ObjectValidator.validate(valid_post_delete, [])
+ assert {:object, {"object not in allowed types", []}} in cng.errors
+ end
+
+ test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do
+ assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, []))
+ end
+
+ test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
+ no_id =
+ valid_post_delete
+ |> Map.delete("id")
+
+ {:error, cng} = ObjectValidator.validate(no_id, [])
+
+ assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
+ end
+
+ test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
+ missing_object =
+ valid_post_delete
+ |> Map.put("object", "http://does.not/exist")
+
+ {:error, cng} = ObjectValidator.validate(missing_object, [])
+
+ assert {:object, {"can't find object", []}} in cng.errors
+ end
+
+ test "it's invalid if the actor of the object and the actor of delete are from different domains",
+ %{valid_post_delete: valid_post_delete} do
+ valid_user = insert(:user)
+
+ valid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", valid_user.ap_id)
+
+ assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
+
+ invalid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+
+ {:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
+
+ assert {:actor, {"is not allowed to delete object", []}} in cng.errors
+ end
+
+ test "it's valid if the actor of the object is a local superuser",
+ %{valid_post_delete: valid_post_delete} do
+ user =
+ insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
+
+ valid_other_actor =
+ valid_post_delete
+ |> Map.put("actor", user.ap_id)
+
+ {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
+ assert meta[:do_not_federate]
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/emoji_react_validation_test.exs b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs
new file mode 100644
index 000000000..582e6d785
--- /dev/null
+++ b/test/web/activity_pub/object_validators/emoji_react_validation_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "EmojiReacts" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+
+ object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
+
+ {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
+
+ %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
+ end
+
+ test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
+ assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
+ end
+
+ test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
+ without_content =
+ valid_emoji_react
+ |> Map.delete("content")
+
+ {:error, cng} = ObjectValidator.validate(without_content, [])
+
+ refute cng.valid?
+ assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
+ end
+
+ test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
+ without_emoji_content =
+ valid_emoji_react
+ |> Map.put("content", "x")
+
+ {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+ refute cng.valid?
+
+ assert {:content, {"must be a single character emoji", []}} in cng.errors
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/follow_validation_test.exs b/test/web/activity_pub/object_validators/follow_validation_test.exs
new file mode 100644
index 000000000..6e1378be2
--- /dev/null
+++ b/test/web/activity_pub/object_validators/follow_validation_test.exs
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+
+ import Pleroma.Factory
+
+ describe "Follows" do
+ setup do
+ follower = insert(:user)
+ followed = insert(:user)
+
+ {:ok, valid_follow, []} = Builder.follow(follower, followed)
+ %{follower: follower, followed: followed, valid_follow: valid_follow}
+ end
+
+ test "validates a basic follow object", %{valid_follow: valid_follow} do
+ assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, [])
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/like_validation_test.exs b/test/web/activity_pub/object_validators/like_validation_test.exs
new file mode 100644
index 000000000..2c033b7e2
--- /dev/null
+++ b/test/web/activity_pub/object_validators/like_validation_test.exs
@@ -0,0 +1,113 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "likes" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+
+ valid_like = %{
+ "to" => [user.ap_id],
+ "cc" => [],
+ "type" => "Like",
+ "id" => Utils.generate_activity_id(),
+ "object" => post_activity.data["object"],
+ "actor" => user.ap_id,
+ "context" => "a context"
+ }
+
+ %{valid_like: valid_like, user: user, post_activity: post_activity}
+ end
+
+ test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
+ {:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
+
+ assert "id" in Map.keys(object)
+ end
+
+ test "is valid for a valid object", %{valid_like: valid_like} do
+ assert LikeValidator.cast_and_validate(valid_like).valid?
+ end
+
+ test "sets the 'to' field to the object actor if no recipients are given", %{
+ valid_like: valid_like,
+ user: user
+ } do
+ without_recipients =
+ valid_like
+ |> Map.delete("to")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_recipients, [])
+
+ assert object["to"] == [user.ap_id]
+ end
+
+ test "sets the context field to the context of the object if no context is given", %{
+ valid_like: valid_like,
+ post_activity: post_activity
+ } do
+ without_context =
+ valid_like
+ |> Map.delete("context")
+
+ {:ok, object, _meta} = ObjectValidator.validate(without_context, [])
+
+ assert object["context"] == post_activity.data["context"]
+ end
+
+ test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
+ without_actor = Map.delete(valid_like, "actor")
+
+ refute LikeValidator.cast_and_validate(without_actor).valid?
+
+ with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
+
+ refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
+ end
+
+ test "it errors when the object is missing or not known", %{valid_like: valid_like} do
+ without_object = Map.delete(valid_like, "object")
+
+ refute LikeValidator.cast_and_validate(without_object).valid?
+
+ with_invalid_object = Map.put(valid_like, "object", "invalidobject")
+
+ refute LikeValidator.cast_and_validate(with_invalid_object).valid?
+ end
+
+ test "it errors when the actor has already like the object", %{
+ valid_like: valid_like,
+ user: user,
+ post_activity: post_activity
+ } do
+ _like = CommonAPI.favorite(user, post_activity.id)
+
+ refute LikeValidator.cast_and_validate(valid_like).valid?
+ end
+
+ test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
+ wrapped_like =
+ valid_like
+ |> Map.put("actor", %{"id" => valid_like["actor"]})
+ |> Map.put("object", %{"id" => valid_like["object"]})
+
+ validated = LikeValidator.cast_and_validate(wrapped_like)
+
+ assert validated.valid?
+
+ assert {:actor, valid_like["actor"]} in validated.changes
+ assert {:object, valid_like["object"]} in validated.changes
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/undo_validation_test.exs b/test/web/activity_pub/object_validators/undo_validation_test.exs
new file mode 100644
index 000000000..75bbcc4b6
--- /dev/null
+++ b/test/web/activity_pub/object_validators/undo_validation_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "Undos" do
+ setup do
+ user = insert(:user)
+ {:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
+ {:ok, like} = CommonAPI.favorite(user, post_activity.id)
+ {:ok, valid_like_undo, []} = Builder.undo(user, like)
+
+ %{user: user, like: like, valid_like_undo: valid_like_undo}
+ end
+
+ test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
+ assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
+ end
+
+ test "it does not validate if the actor of the undo is not the actor of the object", %{
+ valid_like_undo: valid_like_undo
+ } do
+ other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
+
+ bad_actor =
+ valid_like_undo
+ |> Map.put("actor", other_user.ap_id)
+
+ {:error, cng} = ObjectValidator.validate(bad_actor, [])
+
+ assert {:actor, {"not the same as object actor", []}} in cng.errors
+ end
+
+ test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
+ missing_object =
+ valid_like_undo
+ |> Map.put("object", "https://gensokyo.2hu/objects/1")
+
+ {:error, cng} = ObjectValidator.validate(missing_object, [])
+
+ assert {:object, {"can't find object", []}} in cng.errors
+ assert length(cng.errors) == 1
+ end
+ end
+end
diff --git a/test/web/activity_pub/object_validators/update_validation_test.exs b/test/web/activity_pub/object_validators/update_validation_test.exs
new file mode 100644
index 000000000..5e80cf731
--- /dev/null
+++ b/test/web/activity_pub/object_validators/update_validation_test.exs
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.Builder
+ alias Pleroma.Web.ActivityPub.ObjectValidator
+
+ import Pleroma.Factory
+
+ describe "updates" do
+ setup do
+ user = insert(:user)
+
+ object = %{
+ "id" => user.ap_id,
+ "name" => "A new name",
+ "summary" => "A new bio"
+ }
+
+ {:ok, valid_update, []} = Builder.update(user, object)
+
+ %{user: user, valid_update: valid_update}
+ end
+
+ test "validates a basic object", %{valid_update: valid_update} do
+ assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
+ end
+
+ test "returns an error if the object can't be updated by the actor", %{
+ valid_update: valid_update
+ } do
+ other_user = insert(:user)
+
+ update =
+ valid_update
+ |> Map.put("actor", other_user.ap_id)
+
+ assert {:error, _cng} = ObjectValidator.validate(update, [])
+ end
+ end
+end
diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs
index b3b573c9b..9d657ac4f 100644
--- a/test/web/activity_pub/relay_test.exs
+++ b/test/web/activity_pub/relay_test.exs
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
alias Pleroma.Activity
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.CommonAPI
import ExUnit.CaptureLog
import Pleroma.Factory
@@ -53,8 +53,7 @@ test "returns errors when user not found" do
test "returns activity" do
user = insert(:user)
service_actor = Relay.get_actor()
- ActivityPub.follow(service_actor, user)
- Pleroma.User.follow(service_actor, user)
+ CommonAPI.follow(service_actor, user)
assert "#{user.ap_id}/followers" in User.following(service_actor)
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
@@ -74,6 +73,7 @@ test "returns error when activity not `Create` type" do
assert Relay.publish(activity) == {:error, "Not implemented"}
end
+ @tag capture_log: true
test "returns error when activity not public" do
activity = insert(:direct_note_activity)
assert Relay.publish(activity) == {:error, false}
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index 06c39eed6..17e764ca1 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -160,7 +160,7 @@ test "it rejects incoming follow requests if the following errors for some reaso
|> Poison.decode!()
|> Map.put("object", user.ap_id)
- with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do
+ with_mock Pleroma.User, [:passthrough], follow: fn _, _, _ -> {:error, :testing} end do
{:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data)
%Activity{} = activity = Activity.get_by_ap_id(id)
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 6a53fd3f0..f7b7d1a9f 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Object.Fetcher
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
@@ -452,7 +451,7 @@ test "it works for incoming accepts which were pre-accepted" do
{:ok, follower} = User.follow(follower, followed)
assert User.following?(follower, followed) == true
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
@@ -482,7 +481,7 @@ test "it works for incoming accepts which were orphaned" do
follower = insert(:user)
followed = insert(:user, locked: true)
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
@@ -504,7 +503,7 @@ test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user)
followed = insert(:user, locked: true)
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
@@ -569,7 +568,7 @@ test "it works for incoming rejects which are orphaned" do
followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
- {:ok, _follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, _follow_activity} = CommonAPI.follow(follower, followed)
assert User.following?(follower, followed) == true
@@ -595,7 +594,7 @@ test "it works for incoming rejects which are referenced by IRI only" do
followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
- {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
assert User.following?(follower, followed) == true
@@ -659,22 +658,44 @@ test "it remaps video URLs as attachments if necessary" do
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
- attachment = %{
- "type" => "Link",
- "mediaType" => "video/mp4",
- "url" => [
- %{
- "href" =>
- "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
- "mediaType" => "video/mp4"
- }
- ]
- }
-
assert object.data["url"] ==
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
- assert object.data["attachment"] == [attachment]
+ assert object.data["attachment"] == [
+ %{
+ "type" => "Link",
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" =>
+ "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+ "mediaType" => "video/mp4"
+ }
+ ]
+ }
+ ]
+
+ {:ok, object} =
+ Fetcher.fetch_object_from_id(
+ "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"
+ )
+
+ assert object.data["attachment"] == [
+ %{
+ "type" => "Link",
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" =>
+ "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4",
+ "mediaType" => "video/mp4"
+ }
+ ]
+ }
+ ]
+
+ assert object.data["url"] ==
+ "https://framatube.org/videos/watch/6050732a-8a7a-43d4-a6cd-809525a1d206"
end
test "it accepts Flag activities" do
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index 2f9ecb5a3..361dc5a41 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
@@ -197,8 +196,8 @@ test "updates the state of all Follow activities with the same actor and object"
user = insert(:user, locked: true)
follower = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_two} = ActivityPub.follow(follower, user)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
data =
follow_activity_two.data
@@ -221,8 +220,8 @@ test "updates the state of the given follow activity" do
user = insert(:user, locked: true)
follower = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_two} = ActivityPub.follow(follower, user)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
data =
follow_activity_two.data
diff --git a/test/web/admin_api/controllers/admin_api_controller_test.exs b/test/web/admin_api/controllers/admin_api_controller_test.exs
index 48fb108ec..c2433f23c 100644
--- a/test/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/web/admin_api/controllers/admin_api_controller_test.exs
@@ -1514,6 +1514,15 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do
end
end
+ test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated",
+ %{conn: conn} do
+ clear_config(Pleroma.Config.get([:instance, :limit_to_local_content]), :unauthenticated)
+ user = insert(:user, %{local: false, nickname: "u@peer1.com"})
+ conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials")
+
+ assert json_response(conn, 200)
+ end
+
describe "GET /users/:nickname/credentials" do
test "gets the user credentials", %{conn: conn} do
user = insert(:user)
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 908ee5484..7e11fede3 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -934,6 +934,15 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do
end
end
+ describe "follow/2" do
+ test "directly follows a non-locked local user" do
+ [follower, followed] = insert_pair(:user)
+ {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
+
+ assert User.following?(follower, followed)
+ end
+ end
+
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
@@ -998,9 +1007,9 @@ test "after acceptance, it sets all existing pending follow request states to 'a
follower = insert(:user)
follower_two = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_two} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_three} = ActivityPub.follow(follower_two, user)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
@@ -1018,9 +1027,9 @@ test "after rejection, it sets all existing pending follow request states to 're
follower = insert(:user)
follower_two = insert(:user)
- {:ok, follow_activity} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_two} = ActivityPub.follow(follower, user)
- {:ok, follow_activity_three} = ActivityPub.follow(follower_two, user)
+ {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
+ {:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
index 37e33bc33..638626b45 100644
--- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
@@ -223,10 +223,21 @@ test "updates the user's avatar", %{user: user, conn: conn} do
filename: "an_image.jpg"
}
- conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
+ assert user.avatar == %{}
- assert user_response = json_response_and_validate_schema(conn, 200)
+ res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
+
+ assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["avatar"] != User.avatar_url(user)
+
+ user = User.get_by_id(user.id)
+ refute user.avatar == %{}
+
+ # Also resets it
+ _res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""})
+
+ user = User.get_by_id(user.id)
+ assert user.avatar == nil
end
test "updates the user's banner", %{user: user, conn: conn} do
@@ -236,26 +247,39 @@ test "updates the user's banner", %{user: user, conn: conn} do
filename: "an_image.jpg"
}
- conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
+ res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
- assert user_response = json_response_and_validate_schema(conn, 200)
+ assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["header"] != User.banner_url(user)
+
+ # Also resets it
+ _res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""})
+
+ user = User.get_by_id(user.id)
+ assert user.banner == nil
end
- test "updates the user's background", %{conn: conn} do
+ test "updates the user's background", %{conn: conn, user: user} do
new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
- conn =
+ res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_header
})
- assert user_response = json_response_and_validate_schema(conn, 200)
+ assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["pleroma"]["background_image"]
+ #
+ # Also resets it
+ _res =
+ patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""})
+
+ user = User.get_by_id(user.id)
+ assert user.background == nil
end
test "requires 'write:accounts' permission" do
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index 260ad2306..9c7b5e9b2 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -708,7 +708,10 @@ test "following without reblogs" do
followed = insert(:user)
other_user = insert(:user)
- ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false")
+ ret_conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false})
assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200)
@@ -722,7 +725,8 @@ test "following without reblogs" do
assert %{"showing_reblogs" => true} =
conn
- |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true})
|> json_response_and_validate_schema(200)
assert [%{"id" => ^reblog_id}] =
@@ -731,6 +735,35 @@ test "following without reblogs" do
|> json_response(200)
end
+ test "following with reblogs" do
+ %{conn: conn} = oauth_access(["follow", "read:statuses"])
+ followed = insert(:user)
+ other_user = insert(:user)
+
+ ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow")
+
+ assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
+ {:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed)
+
+ assert [%{"id" => ^reblog_id}] =
+ conn
+ |> get("/api/v1/timelines/home")
+ |> json_response(200)
+
+ assert %{"showing_reblogs" => false} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false})
+ |> json_response_and_validate_schema(200)
+
+ assert [] ==
+ conn
+ |> get("/api/v1/timelines/home")
+ |> json_response(200)
+ end
+
test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
@@ -904,7 +937,7 @@ test "Account registration via Application", %{conn: conn} do
%{
"access_token" => token,
"created_at" => _created_at,
- "scope" => _scope,
+ "scope" => ^scope,
"token_type" => "Bearer"
} = json_response_and_validate_schema(conn, 200)
@@ -1066,7 +1099,7 @@ test "registration from trusted app" do
assert %{
"access_token" => access_token,
"created_at" => _,
- "scope" => ["read", "write", "follow", "push"],
+ "scope" => "read write follow push",
"token_type" => "Bearer"
} = response
@@ -1184,7 +1217,7 @@ test "creates an account and returns 200 if captcha is valid", %{conn: conn} do
assert %{
"access_token" => access_token,
"created_at" => _,
- "scope" => ["read"],
+ "scope" => "read",
"token_type" => "Bearer"
} =
conn
diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
index 44e12d15a..6749e0e83 100644
--- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs
+++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
import Pleroma.Factory
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
test "/api/v1/follow_requests works", %{user: user, conn: conn} do
other_user = insert(:user)
- {:ok, _activity} = ActivityPub.follow(other_user, user)
+ {:ok, _, _, _activity} = CommonAPI.follow(other_user, user)
{:ok, other_user} = User.follow(other_user, user, :follow_pending)
assert User.following?(other_user, user) == false
@@ -34,7 +34,7 @@ test "/api/v1/follow_requests works", %{user: user, conn: conn} do
test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do
other_user = insert(:user)
- {:ok, _activity} = ActivityPub.follow(other_user, user)
+ {:ok, _, _, _activity} = CommonAPI.follow(other_user, user)
{:ok, other_user} = User.follow(other_user, user, :follow_pending)
user = User.get_cached_by_id(user.id)
@@ -56,7 +56,7 @@ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do
test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do
other_user = insert(:user)
- {:ok, _activity} = ActivityPub.follow(other_user, user)
+ {:ok, _, _, _activity} = CommonAPI.follow(other_user, user)
user = User.get_cached_by_id(user.id)
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index 95ee26416..cc880d82c 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -32,7 +32,9 @@ test "get instance information", %{conn: conn} do
"avatar_upload_limit" => _,
"background_upload_limit" => _,
"banner_upload_limit" => _,
- "background_image" => _
+ "background_image" => _,
+ "chat_limit" => _,
+ "description_limit" => _
} = result
assert result["pleroma"]["metadata"]["account_activation_required"] != nil
diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs
index f069390c1..50e0d783d 100644
--- a/test/web/mastodon_api/controllers/timeline_controller_test.exs
+++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -418,4 +418,78 @@ test "multi-hashtag timeline", %{conn: conn} do
assert [status_none] == json_response_and_validate_schema(all_test, :ok)
end
end
+
+ describe "hashtag timeline handling of :restrict_unauthenticated setting" do
+ setup do
+ user = insert(:user)
+ {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"})
+ {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"})
+
+ activity1
+ |> Ecto.Changeset.change(%{local: false})
+ |> Pleroma.Repo.update()
+
+ base_uri = "/api/v1/timelines/tag/tag1"
+ error_response = %{"error" => "authorization required for timeline view"}
+
+ %{base_uri: base_uri, error_response: error_response}
+ end
+
+ defp ensure_authenticated_access(base_uri) do
+ %{conn: auth_conn} = oauth_access(["read:statuses"])
+
+ res_conn = get(auth_conn, "#{base_uri}?local=true")
+ assert length(json_response(res_conn, 200)) == 1
+
+ res_conn = get(auth_conn, "#{base_uri}?local=false")
+ assert length(json_response(res_conn, 200)) == 2
+ end
+
+ test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{
+ conn: conn,
+ base_uri: base_uri,
+ error_response: error_response
+ } do
+ clear_config([:restrict_unauthenticated, :timelines, :local], true)
+ clear_config([:restrict_unauthenticated, :timelines, :federated], true)
+
+ for local <- [true, false] do
+ res_conn = get(conn, "#{base_uri}?local=#{local}")
+
+ assert json_response(res_conn, :unauthorized) == error_response
+ end
+
+ ensure_authenticated_access(base_uri)
+ end
+
+ test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline",
+ %{conn: conn, base_uri: base_uri, error_response: error_response} do
+ clear_config([:restrict_unauthenticated, :timelines, :local], false)
+ clear_config([:restrict_unauthenticated, :timelines, :federated], true)
+
+ res_conn = get(conn, "#{base_uri}?local=true")
+ assert length(json_response(res_conn, 200)) == 1
+
+ res_conn = get(conn, "#{base_uri}?local=false")
+ assert json_response(res_conn, :unauthorized) == error_response
+
+ ensure_authenticated_access(base_uri)
+ end
+
+ test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <>
+ "(but not to local public activities which are delivered as part of federated timeline)",
+ %{conn: conn, base_uri: base_uri, error_response: error_response} do
+ clear_config([:restrict_unauthenticated, :timelines, :local], true)
+ clear_config([:restrict_unauthenticated, :timelines, :federated], false)
+
+ res_conn = get(conn, "#{base_uri}?local=true")
+ assert json_response(res_conn, :unauthorized) == error_response
+
+ # Note: local activities get delivered as part of federated timeline
+ res_conn = get(conn, "#{base_uri}?local=false")
+ assert length(json_response(res_conn, 200)) == 2
+
+ ensure_authenticated_access(base_uri)
+ end
+ end
end
diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs
index a7f9c5205..c08be37d4 100644
--- a/test/web/mastodon_api/mastodon_api_test.exs
+++ b/test/web/mastodon_api/mastodon_api_test.exs
@@ -18,7 +18,7 @@ test "returns error when followed user is deactivated" do
follower = insert(:user)
user = insert(:user, local: true, deactivated: true)
{:error, error} = MastodonAPI.follow(follower, user)
- assert error == "Could not follow user: #{user.nickname} is deactivated."
+ assert error == :rejected
end
test "following for user" do
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 4aba6aaf1..17f035add 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase
+ alias Pleroma.Config
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
@@ -18,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
:ok
end
+ setup do: clear_config([:instances_favicons, :enabled])
+
test "Represent a user account" do
background_image = %{
"url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}]
@@ -75,6 +78,8 @@ test "Represent a user account" do
pleroma: %{
ap_id: user.ap_id,
background_image: "https://example.com/images/asuka_hospital.png",
+ favicon:
+ "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
confirmation_pending: false,
tags: [],
is_admin: false,
@@ -93,6 +98,23 @@ test "Represent a user account" do
assert expected == AccountView.render("show.json", %{user: user})
end
+ test "Favicon is nil when :instances_favicons is disabled" do
+ user = insert(:user)
+
+ Config.put([:instances_favicons, :enabled], true)
+
+ assert %{
+ pleroma: %{
+ favicon:
+ "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png"
+ }
+ } = AccountView.render("show.json", %{user: user})
+
+ Config.put([:instances_favicons, :enabled], false)
+
+ assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user})
+ end
+
test "Represent the user account for the account owner" do
user = insert(:user)
@@ -153,6 +175,8 @@ test "Represent a Service(bot) account" do
pleroma: %{
ap_id: user.ap_id,
background_image: nil,
+ favicon:
+ "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
confirmation_pending: false,
tags: [],
is_admin: false,
@@ -374,6 +398,9 @@ test "shows actual follower/following count to the account owner" do
user = insert(:user, hide_followers: true, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
+
+ assert User.following?(user, other_user)
+ assert Pleroma.FollowingRelationship.follower_count(other_user) == 1
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs
index 103997c31..07909d48b 100644
--- a/test/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -13,8 +13,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
import Pleroma.Factory
import Swoosh.TestAssertions
- @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
-
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do
{:ok, user} =
@@ -68,103 +66,6 @@ test "resend account confirmation email (with nickname)", %{conn: conn, user: us
end
end
- describe "PATCH /api/v1/pleroma/accounts/update_avatar" do
- setup do: oauth_access(["write:accounts"])
-
- test "user avatar can be set", %{user: user, conn: conn} do
- avatar_image = File.read!("test/fixtures/avatar_data_uri")
-
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
-
- user = refresh_record(user)
-
- assert %{
- "name" => _,
- "type" => _,
- "url" => [
- %{
- "href" => _,
- "mediaType" => _,
- "type" => _
- }
- ]
- } = user.avatar
-
- assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
- end
-
- test "user avatar can be reset", %{user: user, conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""})
-
- user = User.get_cached_by_id(user.id)
-
- assert user.avatar == nil
-
- assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
- end
- end
-
- describe "PATCH /api/v1/pleroma/accounts/update_banner" do
- setup do: oauth_access(["write:accounts"])
-
- test "can set profile banner", %{user: user, conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
-
- user = refresh_record(user)
- assert user.banner["type"] == "Image"
-
- assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
- end
-
- test "can reset profile banner", %{user: user, conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
-
- user = refresh_record(user)
- assert user.banner == %{}
-
- assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
- end
- end
-
- describe "PATCH /api/v1/pleroma/accounts/update_background" do
- setup do: oauth_access(["write:accounts"])
-
- test "background image can be set", %{user: user, conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image})
-
- user = refresh_record(user)
- assert user.background["type"] == "Image"
- # assert %{"url" => _} = json_response(conn, 200)
- assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
- end
-
- test "background image can be reset", %{user: user, conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "multipart/form-data")
- |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""})
-
- user = refresh_record(user)
- assert user.background == %{}
- assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
- end
- end
-
describe "getting favorites timeline of specified user" do
setup do
[current_user, user] = insert_pair(:user, hide_favorites: false)
diff --git a/test/web/preload/status_net_test.exs b/test/web/preload/status_net_test.exs
deleted file mode 100644
index df7acdb11..000000000
--- a/test/web/preload/status_net_test.exs
+++ /dev/null
@@ -1,15 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Preload.Providers.StatusNetTest do
- use Pleroma.DataCase
- alias Pleroma.Web.Preload.Providers.StatusNet
-
- setup do: {:ok, StatusNet.generate_terms(nil)}
-
- test "it renders the info", %{"/api/statusnet/config.json" => info} do
- assert {:ok, res} = Jason.decode(info)
- assert res["site"]
- end
-end
diff --git a/test/web/static_fe/static_fe_controller_test.exs b/test/web/static_fe/static_fe_controller_test.exs
index a49ab002f..1598bf675 100644
--- a/test/web/static_fe/static_fe_controller_test.exs
+++ b/test/web/static_fe/static_fe_controller_test.exs
@@ -87,6 +87,20 @@ test "single notice page", %{conn: conn, user: user} do
assert html =~ "testing a thing!"
end
+ test "redirects to json if requested", %{conn: conn, user: user} do
+ {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
+
+ conn =
+ conn
+ |> put_req_header(
+ "accept",
+ "Accept: application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", text/html"
+ )
+ |> get("/notice/#{activity.id}")
+
+ assert redirected_to(conn, 302) =~ activity.data["object"]
+ end
+
test "filters HTML tags", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ""})
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index ad919d341..76e9369f7 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -224,105 +224,6 @@ test "it updates notification privacy option", %{user: user, conn: conn} do
end
end
- describe "GET /api/statusnet/config" do
- test "it returns config in xml format", %{conn: conn} do
- instance = Config.get(:instance)
-
- response =
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/api/statusnet/config")
- |> response(:ok)
-
- assert response ==
- "\n\n#{Keyword.get(instance, :name)} \n#{
- Pleroma.Web.base_url()
- } \n#{Keyword.get(instance, :limit)} \n#{
- !Keyword.get(instance, :registrations_open)
- } \n \n \n"
- end
-
- test "it returns config in json format", %{conn: conn} do
- instance = Config.get(:instance)
- Config.put([:instance, :managed_config], true)
- Config.put([:instance, :registrations_open], false)
- Config.put([:instance, :invites_enabled], true)
- Config.put([:instance, :public], false)
- Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
-
- response =
- conn
- |> put_req_header("accept", "application/json")
- |> get("/api/statusnet/config")
- |> json_response(:ok)
-
- expected_data = %{
- "site" => %{
- "accountActivationRequired" => "0",
- "closed" => "1",
- "description" => Keyword.get(instance, :description),
- "invitesEnabled" => "1",
- "name" => Keyword.get(instance, :name),
- "pleromafe" => %{"theme" => "asuka-hospital"},
- "private" => "1",
- "safeDMMentionsEnabled" => "0",
- "server" => Pleroma.Web.base_url(),
- "textlimit" => to_string(Keyword.get(instance, :limit)),
- "uploadlimit" => %{
- "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)),
- "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)),
- "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)),
- "uploadlimit" => to_string(Keyword.get(instance, :upload_limit))
- },
- "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
- }
- }
-
- assert response == expected_data
- end
-
- test "returns the state of safe_dm_mentions flag", %{conn: conn} do
- Config.put([:instance, :safe_dm_mentions], true)
-
- response =
- conn
- |> get("/api/statusnet/config.json")
- |> json_response(:ok)
-
- assert response["site"]["safeDMMentionsEnabled"] == "1"
-
- Config.put([:instance, :safe_dm_mentions], false)
-
- response =
- conn
- |> get("/api/statusnet/config.json")
- |> json_response(:ok)
-
- assert response["site"]["safeDMMentionsEnabled"] == "0"
- end
-
- test "it returns the managed config", %{conn: conn} do
- Config.put([:instance, :managed_config], false)
- Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
-
- response =
- conn
- |> get("/api/statusnet/config.json")
- |> json_response(:ok)
-
- refute response["site"]["pleromafe"]
-
- Config.put([:instance, :managed_config], true)
-
- response =
- conn
- |> get("/api/statusnet/config.json")
- |> json_response(:ok)
-
- assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"}
- end
- end
-
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
@@ -451,28 +352,6 @@ test "with valid permissions and invalid password, it returns an error", %{conn:
end
end
- describe "GET /api/statusnet/version" do
- test "it returns version in xml format", %{conn: conn} do
- response =
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/api/statusnet/version")
- |> response(:ok)
-
- assert response == "#{Pleroma.Application.named_version()} "
- end
-
- test "it returns version in json format", %{conn: conn} do
- response =
- conn
- |> put_req_header("accept", "application/json")
- |> get("/api/statusnet/version")
- |> json_response(:ok)
-
- assert response == "#{Pleroma.Application.named_version()}"
- end
- end
-
describe "POST /main/ostatus - remote_subscribe/2" do
setup do: clear_config([:instance, :federating], true)