Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into chat-federation-information

This commit is contained in:
lain 2020-07-10 12:26:53 +02:00
commit b39eb6ecc5
206 changed files with 2662 additions and 1780 deletions

View File

@ -63,21 +63,19 @@ unit-testing:
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --preload-modules - mix coveralls --preload-modules
# Removed to fix CI issue. In this early state it wasn't adding much value anyway. federated-testing:
# TODO Fix and reinstate federated testing stage: test
# federated-testing: cache: *testing_cache_policy
# stage: test services:
# cache: *testing_cache_policy - name: minibikini/postgres-with-rum:12
# services: alias: postgres
# - name: minibikini/postgres-with-rum:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
# alias: postgres script:
# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - mix deps.get
# script: - mix ecto.create
# - mix deps.get - mix ecto.migrate
# - mix ecto.create - epmd -daemon
# - mix ecto.migrate - mix test --trace --only federated
# - epmd -daemon
# - mix test --trace --only federated
unit-testing-rum: unit-testing-rum:
stage: test stage: test

View File

@ -16,10 +16,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- **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. - **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. - 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: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text. - Mastodon API: On deletion, returns the original post text.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
</details> </details>
<details> <details>
@ -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 - Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - 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 pagination in emoji packs API (for packs and for files in pack)
- Support for viewing instances favicons next to posts and accounts
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -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: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines - 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 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: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view. - Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs - 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 - Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP - `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set - 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)] ## [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: `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: 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, 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 - 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: 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. - 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.

View File

@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do
@visibility ~w(public private direct unlisted) @visibility ~w(public private direct unlisted)
@types [ @types [
:simple, :simple,
:simple_filtered,
:emoji, :emoji,
:mentions, :mentions,
:hell_thread, :hell_thread,
@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status") insert_local_activity(visibility, group, users, "Simple status")
end 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) defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")

View File

@ -32,10 +32,22 @@ defp fetch_user(user) do
) )
end 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 defp fetch_timelines(user) do
fetch_home_timeline(user) fetch_home_timeline(user)
fetch_home_timeline_with_filter(user)
fetch_direct_timeline(user) fetch_direct_timeline(user)
fetch_public_timeline(user) fetch_public_timeline(user)
fetch_public_timeline_with_filter(user)
fetch_public_timeline(user, :with_blocks) fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local) fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag) fetch_public_timeline(user, :tag)
@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do
} }
end end
defp fetch_home_timeline(user) do defp fetch_home_timeline(user, title_end \\ "") do
opts = opts_for_home_timeline(user) opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)] recipients = [user.ap_id | User.following(user)]
@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do
|> Enum.reverse() |> Enum.reverse()
|> List.last() |> List.last()
title = "home timeline " <> title_end
Benchee.run( Benchee.run(
%{ %{
"home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
}, },
inputs: %{ inputs: %{
"1 page" => opts, "1 page" => opts,
@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do
) )
end 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 defp opts_for_direct_timeline(user) do
%{ %{
visibility: "direct", visibility: "direct",
@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do
fetch_public_timeline(opts, "public timeline") fetch_public_timeline(opts, "public timeline")
end 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 defp fetch_public_timeline(user, :local) do
opts = opts_for_public_timeline(user, :local) opts = opts_for_public_timeline(user, :local)

View File

@ -97,6 +97,7 @@
"dat", "dat",
"dweb", "dweb",
"gopher", "gopher",
"hyper",
"ipfs", "ipfs",
"ipns", "ipns",
"irc", "irc",
@ -188,6 +189,7 @@
background_image: "/images/city.jpg", background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg", instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000, limit: 5_000,
description_limit: 5_000,
chat_limit: 5_000, chat_limit: 5_000,
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
@ -436,8 +438,7 @@
config :pleroma, Pleroma.Web.Preload, config :pleroma, Pleroma.Web.Preload,
providers: [ providers: [
Pleroma.Web.Preload.Providers.Instance, Pleroma.Web.Preload.Providers.Instance
Pleroma.Web.Preload.Providers.StatusNet
] ]
config :pleroma, :http_security, config :pleroma, :http_security,
@ -705,6 +706,8 @@
config :ex_aws, http_client: Pleroma.HTTP.ExAws config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :pleroma, :instances_favicons, enabled: false
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View File

@ -498,6 +498,7 @@
"dat", "dat",
"dweb", "dweb",
"gopher", "gopher",
"hyper",
"ipfs", "ipfs",
"ipns", "ipns",
"irc", "irc",
@ -699,8 +700,9 @@
key: :public, key: :public,
type: :boolean, type: :boolean,
description: description:
"Makes the client API in authentificated mode-only except for user-profiles." <> "Makes the client API in authenticated mode-only except for user-profiles." <>
" Useful for disabling the Local Timeline and The Whole Known Network." " 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, key: :quarantined_instances,
@ -3446,5 +3448,18 @@
suggestions: [false] 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"
}
]
} }
] ]

View File

@ -111,6 +111,8 @@
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
config :pleroma, :instances_favicons, enabled: true
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View File

@ -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. - `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. - `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 - `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 ### 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. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `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. - `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account. - `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages. - `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 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. 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 `GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post - `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 - `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size - `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars - `avatar_upload_limit`: The same for avatars

View File

@ -57,11 +57,11 @@ mix pleroma.user invites
## Revoke invite ## Revoke invite
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user revoke_invite <token_or_id> ./bin/pleroma_ctl user revoke_invite <token>
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.user revoke_invite <token_or_id> mix pleroma.user revoke_invite <token>
``` ```

View File

@ -1,6 +1,6 @@
# Updating your instance # 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: Besides that, doing the following is generally enough:

View File

@ -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. * `notify_email`: Email used for notifications.
* `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``. * `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``.
* `limit`: Posts character limit (CW/Subject included in the counter). * `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. * `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner). * `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_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. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance. * `allow_relay`: Enable Pleromas 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. * `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``. * `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). * `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 * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely * `:reject` rejects the message entirely
#### mrf_steal_emoji #### :mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from * `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject * `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk * `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_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 * `timelines`: public and federated timelines
* `local`: public timeline * `local`: public timeline
* `federated` * `federated`: federated timeline (includes public timeline)
* `profiles`: user profiles * `profiles`: user profiles
* `local` * `local`
* `remote` * `remote`
@ -982,7 +983,14 @@ Restrict access for unauthenticated users to timelines (public and federate), us
* `local` * `local`
* `remote` * `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 ## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`. * `: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

View File

@ -3,15 +3,48 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do 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" @doc "Common functions to be reused in mix tasks"
def start_pleroma do def start_pleroma do
Pleroma.Config.Holder.save_default()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug) Application.put_env(:logger, :console, level: :debug)
end 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 if Pleroma.Config.get(:env) not in [:test, :benchmark] do
pleroma_rebooted?() pleroma_rebooted?()

View File

@ -145,7 +145,7 @@ def run(["gen" | rest]) do
options, options,
:uploads_dir, :uploads_dir,
"What directory should media uploads go in (when using the local uploader)?", "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() |> Path.expand()
@ -154,7 +154,7 @@ def run(["gen" | rest]) do
options, options,
:static_dir, :static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", "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() |> Path.expand()

View File

@ -232,7 +232,7 @@ def run(["tag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags) user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") 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 with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags) user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") shell_error("Could not change user tags for #{nickname}")

View File

@ -35,7 +35,7 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
Pleroma.Config.Holder.save_default() Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers() Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn() Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
@ -162,7 +162,8 @@ defp idempotency_expiration,
defp seconds_valid_interval, defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) 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: %{ do: %{
id: String.to_atom("cachex_" <> type), id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},

View File

@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, :gopher, [:enabled]} {:pleroma, :gopher, [:enabled]}
] ]
def start_link(_) do def start_link(restart_pleroma? \\ true) do
load_and_update_env() load_and_update_env([], restart_pleroma?)
if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore :ignore
end end

View File

@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Web.Router.Helpers 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_name, do: instance_config()[:name]
defp instance_notify_email do defp instance_notify_email do
@ -72,6 +72,8 @@ def report(to, reporter, account, statuses, comment) do
<p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p> <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
#{comment_html} #{comment_html}
#{statuses_html} #{statuses_html}
<p>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
""" """
new() new()

View File

@ -108,7 +108,7 @@ defp load_pack(pack_dir, emoji_groups) do
if File.exists?(emoji_txt) do if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups) load_from_file(emoji_txt, emoji_groups)
else else
extensions = Pleroma.Config.get([:emoji, :pack_extensions]) extensions = Config.get([:emoji, :pack_extensions])
Logger.info( Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{ "No emoji.txt found for pack \"#{pack_name}\", assuming all #{

View File

@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do
Repo.one(query) Repo.one(query)
end 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 = query =
from( from(
f in Pleroma.Filter, f in query,
where: f.user_id == ^user_id, where: f.user_id == ^user_id,
order_by: [desc: :id] order_by: [desc: :id]
) )
@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do
|> validate_required([:phrase, :context]) |> validate_required([:phrase, :context])
|> Repo.update() |> Repo.update()
end 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 end

View File

@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do schema "instances" do
field(:host, :string) field(:host, :string)
field(:unreachable_since, :naive_datetime_usec) field(:unreachable_since, :naive_datetime_usec)
field(:favicon, :string)
field(:favicon_updated_at, :naive_datetime)
timestamps() timestamps()
end end
@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do def changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:host, :unreachable_since]) |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|> validate_required([:host]) |> validate_required([:host])
|> unique_constraint(:host) |> unique_constraint(:host)
end end
@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do
end end
defp parse_datetime(datetime), do: datetime 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 end

View File

@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do
|> preload([n, a, o], activity: {a, object: o}) |> preload([n, a, o], activity: {a, object: o})
|> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts) |> exclude_blocked(user, exclude_blocked_opts)
|> exclude_filtered(user)
|> exclude_visibility(opts) |> exclude_visibility(opts)
end end
@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do
|> where([n, a, o, tm], is_nil(tm.user_id)) |> where([n, a, o, tm], is_nil(tm.user_id))
end 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] @valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility}) defp exclude_visibility(query, %{exclude_visibilities: visibility})
@ -337,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do
end end
end end
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
def create_notifications(activity, options \\ []) def create_notifications(activity, options \\ [])
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do 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
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 def get_potential_receiver_ap_ids(activity) do
[] []
|> Utils.maybe_notify_to_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)
@ -555,7 +575,8 @@ def skip?(%Activity{} = activity, %User{} = user) do
:follows, :follows,
:non_followers, :non_followers,
:non_follows, :non_follows,
:recently_followed :recently_followed,
:filtered
] ]
|> Enum.find(&skip?(&1, activity, user)) |> Enum.find(&skip?(&1, activity, user))
end end
@ -624,6 +645,26 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
end) end)
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 skip?(_, _, _), do: false
def for_user_and_activity(user, activity) do def for_user_and_activity(user, activity) do

View File

@ -69,10 +69,11 @@ defp csp_string do
img_src = "img-src 'self' data: blob:" img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'" media_src = "media-src 'self'"
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src} = {img_src, media_src} =
if Config.get([:media_proxy, :enabled]) && if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do !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]} {[img_src, sources], [media_src, sources]}
else else
{[img_src, " https:"], [media_src, " https:"]} {[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 = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
connect_src = connect_src =
if Pleroma.Config.get(:env) == :dev do if Config.get(:env) == :dev do
[connect_src, " http://localhost:3035/"] [connect_src, " http://localhost:3035/"]
else else
connect_src connect_src
end end
script_src = script_src =
if Pleroma.Config.get(:env) == :dev do if Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'" "script-src 'self' 'unsafe-eval'"
else else
"script-src 'self'" "script-src 'self'"
@ -107,29 +108,28 @@ defp csp_string do
|> :erlang.iolist_to_binary() |> :erlang.iolist_to_binary()
end end
defp get_proxy_and_attachment_sources do defp build_csp_multimedia_source_list do
media_proxy_whitelist = media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host) add_source(acc, host)
end) end)
media_proxy_base_url = media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url]))
if Config.get([:media_proxy, :base_url]),
do: URI.parse(Config.get([:media_proxy, :base_url])).host
upload_base_url = upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url]))
if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
s3_endpoint = s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint]))
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host 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(media_proxy_base_url)
|> add_source(upload_base_url) |> add_source(upload_base_url)
|> add_source(s3_endpoint) |> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist) |> add_source(media_proxy_whitelist)
|> add_source(captcha_endpoint)
end end
defp add_source(iodata, nil), do: iodata 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 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 def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do unless Config.get([:http_security, :enabled]) do
Logger.warn(" Logger.warn("

View File

@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
def init(options), do: options def init(options), do: options
def call(conn, _) do def call(conn, _) do
if enabled?() and accepts_html?(conn) do if enabled?() and requires_html?(conn) do
conn conn
|> StaticFEController.call(:show) |> StaticFEController.call(:show)
|> halt() |> halt()
@ -20,10 +20,7 @@ def call(conn, _) do
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
defp accepts_html?(conn) do defp requires_html?(conn) do
case get_req_header(conn, "accept") do Phoenix.Controller.get_format(conn) == "html"
[accept | _] -> String.contains?(accept, "text/html")
_ -> false
end
end end
end end

View File

@ -63,6 +63,10 @@ def store(upload, opts \\ []) do
with {:ok, upload} <- prepare_upload(upload, opts), with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), {: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, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok, {:ok,
%{ %{
@ -75,9 +79,12 @@ def store(upload, opts \\ []) do
"href" => url_from_spec(upload, opts.base_url, url_spec) "href" => url_from_spec(upload, opts.base_url, url_spec)
} }
], ],
"name" => Map.get(opts, :description) || upload.name "name" => description
}} }}
else else
{:description_limit, _} ->
{:error, :description_too_long}
{:error, error} -> {:error, error} ->
Logger.error( Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"

View File

@ -89,7 +89,7 @@ defmodule Pleroma.User do
field(:keys, :string) field(:keys, :string)
field(:public_key, :string) field(:public_key, :string)
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map) field(:avatar, :map, default: %{})
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:follower_address, :string) field(:follower_address, :string)
field(:following_address, :string) field(:following_address, :string)
@ -389,8 +389,8 @@ defp fix_follower_address(%{nickname: nickname} = params),
defp fix_follower_address(params), do: params defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
name = name =
case params[:name] do case params[:name] do
@ -450,8 +450,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
end end
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
struct struct
|> cast( |> cast(
@ -542,14 +542,11 @@ defp put_emoji(changeset) do
end end
defp put_change_if_present(changeset, map_field, value_function) do defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do with {:ok, value} <- fetch_change(changeset, map_field),
with {:ok, new_value} <- value_function.(value) do {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value) put_change(changeset, map_field, new_value)
else
_ -> changeset
end
else else
changeset _ -> changeset
end end
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 force_password_reset(user), do: update_password_reset_pending(user, true)
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
params = Map.put_new(params, :accepts_chat_messages, true) params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? = need_confirmation? =
if is_nil(opts[:need_confirmation]) do if is_nil(opts[:need_confirmation]) do
Pleroma.Config.get([:instance, :account_activation_required]) Config.get([:instance, :account_activation_required])
else else
opts[:need_confirmation] opts[:need_confirmation]
end end
@ -652,7 +649,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> unique_constraint(:email) |> unique_constraint(:email)
|> unique_constraint(:nickname) |> 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(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit) |> 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, true), do: changeset
def maybe_validate_required_email(changeset, _) do 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]) validate_required(changeset, [:email])
else else
changeset changeset
@ -687,7 +684,7 @@ defp put_following_and_follower_address(changeset) do
end end
defp autofollow_users(user) do defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false}) 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 def try_send_confirmation_email(%User{} = user) do
if user.confirmation_pending && if user.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do Config.get([:instance, :account_activation_required]) do
user user
|> Pleroma.Emails.UserEmail.account_confirmation_email() |> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async() |> Pleroma.Emails.Mailer.deliver_async()
@ -771,7 +768,7 @@ def follow_all(follower, followeds) do
defdelegate following(user), to: FollowingRelationship defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do 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 cond do
followed.deactivated -> followed.deactivated ->
@ -972,7 +969,7 @@ def get_cached_by_nickname(nickname) do
end end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do 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 cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> 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()} @spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do 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) follower_count = FollowingRelationship.follower_count(user)
user user
@ -1181,7 +1178,7 @@ def update_follower_count(%User{} = user) do
@spec update_following_count(User.t()) :: {:ok, User.t()} @spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do 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)} {:ok, maybe_fetch_follow_information(user)}
else else
{:ok, user} {:ok, user}
@ -1268,7 +1265,7 @@ def unmute(%User{} = muter, %User{} = mutee) do
end end
def subscribe(%User{} = subscriber, %User{} = target) do 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 if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"} {:error, "Could not subscribe: #{target.nickname} is blocking you"}
@ -1551,7 +1548,7 @@ def perform(:follow_import, %User{} = follower, followed_identifiers)
fn followed_identifier -> fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed), {:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed followed
else else
err -> err ->
@ -1659,7 +1656,7 @@ def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
end 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) 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 end
defp local_nickname_regex do 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 @extended_local_nickname_regex
else else
@strict_local_nickname_regex @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 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default # use instance-default
config = Pleroma.Config.get([:assets, :mascots]) config = Config.get([:assets, :mascots])
default_mascot = Pleroma.Config.get([:assets, :default_mascot]) default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, 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 def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields 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 changeset
|> validate_length(:fields, max: limit) |> validate_length(:fields, max: limit)
@ -2079,8 +2076,8 @@ def validate_fields(changeset, remote? \\ false) do
end end
defp valid_field?(%{"name" => name, "value" => value}) do defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) name_limit = Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit && is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit String.length(value) <= value_limit
@ -2090,10 +2087,10 @@ defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} = {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} = {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} %{"name" => name, "value" => value}
end end
@ -2148,7 +2145,7 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities 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]} params = %{pinned_activities: user.pinned_activities ++ [id]}
user user

View File

@ -69,11 +69,15 @@ defp fts_search(query, query_string) do
u in query, u in query,
where: where:
fragment( 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.nickname,
u.name,
^query_string ^query_string
) )
) )
@ -88,15 +92,23 @@ defp to_tsquery(query_string) do
|> Enum.join(" | ") |> Enum.join(" | ")
end end
# Considers nickname match, localized nickname match, name match; preferences nickname match
defp trigram_rank(query, query_string) do defp trigram_rank(query, query_string) do
from( from(
u in query, u in query,
select_merge: %{ select_merge: %{
search_rank: search_rank:
fragment( fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))", """
similarity(?, ?) +
similarity(?, regexp_replace(?, '@.+', '')) +
similarity(?, trim(coalesce(?, '')))
""",
^query_string, ^query_string,
u.nickname, u.nickname,
^query_string,
u.nickname,
^query_string,
u.name u.name
) )
} }

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Constants alias Pleroma.Constants
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -321,28 +322,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
end end
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()) :: @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()} {:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do 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) |> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_recipients(recipients, opts[:user]) |> restrict_recipients(recipients, opts[:user])
|> restrict_filtered(opts)
|> where( |> where(
[activity], [activity],
fragment( fragment(
@ -961,6 +941,26 @@ defp restrict_instance(query, %{instance: instance}) do
defp restrict_instance(query, _), do: query 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, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do defp exclude_poll_votes(query, _) do
@ -1091,6 +1091,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts) |> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts) |> restrict_muted(restrict_muted_opts)
|> restrict_filtered(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config) |> restrict_thread_visibility(opts, config)
@ -1099,6 +1100,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts) |> restrict_instance(opts)
|> restrict_announce_object_actor(opts) |> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_chat_messages(opts) |> exclude_chat_messages(opts)

View File

@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants 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()} @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do with {:ok, data, meta} <- object_action(actor, object) do

View File

@ -98,7 +98,7 @@ def filter(message), do: {:ok, message}
@impl true @impl true
def describe do def describe do
mrf_object_age = mrf_object_age =
Pleroma.Config.get(:mrf_object_age) Config.get(:mrf_object_age)
|> Enum.into(%{}) |> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}} {:ok, %{mrf_object_age: mrf_object_age}}

View File

@ -47,5 +47,5 @@ def filter(object), do: {:ok, object}
@impl true @impl true
def describe, def describe,
do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
end end

View File

@ -155,7 +155,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
%{host: actor_host} = URI.parse(actor) %{host: actor_host} = URI.parse(actor)
reject_deletes = reject_deletes =
Pleroma.Config.get([:mrf_simple, :reject_deletes]) Config.get([:mrf_simple, :reject_deletes])
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do if MRF.subdomain_match?(reject_deletes, actor_host) do

View File

@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator 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()} @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta) 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 def validate(%{"type" => "Block"} = block_activity, meta) do
with {:ok, block_activity} <- with {:ok, block_activity} <-
block_activity block_activity

View File

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -28,7 +28,7 @@ def relay_ap_id do
def follow(target_instance) do def follow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {: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"]}") Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Activity.Ir.Topics alias Pleroma.Activity.Ir.Topics
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Chat.MessageReference alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -21,6 +22,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(object, meta \\ []) 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: # Tasks this handles:
# - Unfollow and block # - Unfollow and block
def handle( def handle(
@ -209,14 +273,20 @@ def handle_object_creation(object) do
{:ok, object} {:ok, object}
end end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do defp undo_like(nil, object), do: delete_object(object)
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object), defp undo_like(%Object{} = liked_object, object) do
{:ok, _} <- Repo.delete(object) do with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
:ok delete_object(object)
end end
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 def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_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]} 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 defp send_notifications(meta) do
Keyword.get(meta, :notifications, []) Keyword.get(meta, :notifications, [])
|> Enum.each(fn notification -> |> Enum.each(fn notification ->

View File

@ -233,18 +233,24 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
is_map(url) && is_binary(url["href"]) -> url["href"] is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"] is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"] is_binary(data["href"]) -> data["href"]
true -> nil
end end
attachment_url = if href do
%{"href" => href} attachment_url =
|> Maps.put_if_present("mediaType", media_type) %{"href" => href}
|> Maps.put_if_present("type", Map.get(url || %{}, "type")) |> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]} %{"url" => [attachment_url]}
|> Maps.put_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("type", data["type"]) |> Maps.put_if_present("type", data["type"])
|> Maps.put_if_present("name", data["name"]) |> Maps.put_if_present("name", data["name"])
else
nil
end
end) end)
|> Enum.filter(& &1)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
end 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) def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type in ["Video", "Audio"] and is_list(url) do 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 object
|> Map.put("attachment", [first_element]) |> Map.put("attachment", [attachment])
|> Map.put("url", link_element["href"]) |> Map.put("url", link_element["href"])
end end
@ -517,66 +529,6 @@ def handle_incoming(
end end
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( def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options _options
@ -684,7 +636,7 @@ def handle_incoming(
%{"type" => type} = data, %{"type" => type} = data,
_options _options
) )
when type in ~w{Update Block} do when type in ~w{Update Block Follow} do
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}

View File

@ -206,8 +206,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
end end
end end
def user_show(conn, %{"nickname" => nickname}) do def user_show(%{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 conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("show.json", %{user: user}) |> 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}) |> render("index.json", %{activities: activities, as: :activity})
end 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 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == 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) {_, page_size} = page_params(params)
activities = activities =
@ -526,7 +526,7 @@ def disable_mfa(conn, %{"nickname" => nickname}) do
@doc "Show a given user's credentials" @doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do 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 conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin}) |> render("credentials.json", %{user: user, for: admin})

View File

@ -203,14 +203,23 @@ def follow_operation do
security: [%{"oAuth" => ["follow", "write:follows"]}], security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Follow the given account", description: "Follow the given account",
parameters: [ parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
Operation.parameter(
:reblogs,
:query,
BooleanLike,
"Receive this account's reblogs in home timeline? Defaults to true."
)
], ],
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: %{ responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship), 200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError), 400 => Operation.response("Error", "application/json", ApiError),
@ -438,6 +447,7 @@ defp create_request do
} }
end end
# TODO: This is actually a token respone, but there's no oauth operation file yet.
defp create_response do defp create_response do
%Schema{ %Schema{
title: "AccountCreateResponse", title: "AccountCreateResponse",
@ -446,14 +456,20 @@ defp create_response do
properties: %{ properties: %{
token_type: %Schema{type: :string}, token_type: %Schema{type: :string},
access_token: %Schema{type: :string}, access_token: %Schema{type: :string},
scope: %Schema{type: :array, items: %Schema{type: :string}}, refresh_token: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"} scope: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"},
me: %Schema{type: :string},
expires_in: %Schema{type: :integer}
}, },
example: %{ example: %{
"token_type" => "Bearer",
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
"refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
"created_at" => 1_585_918_714, "created_at" => 1_585_918_714,
"scope" => ["read", "write", "follow", "push"], "expires_in" => 600,
"token_type" => "Bearer" "scope" => "read write follow push",
"me" => "https://gensokyo.2hu/users/raymoo"
} }
} }
end end

View File

@ -4,7 +4,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
@ -40,48 +39,6 @@ def confirmation_resend_operation do
} }
end 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 def favourites_operation do
%Operation{ %Operation{
tags: ["Accounts"], tags: ["Accounts"],
@ -136,52 +93,4 @@ defp id_param do
required: true required: true
) )
end 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 end

View File

@ -103,7 +103,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description: description:
"A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" "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{ source: %Schema{

View File

@ -101,10 +101,14 @@ def unblock(blocker, blocked) do
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follower} <- User.maybe_direct_follow(follower, followed), with {:ok, follow_data, _} <- Builder.follow(follower, followed),
{:ok, activity} <- ActivityPub.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} <- 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
end end

View File

@ -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) def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do 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), with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, 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(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do 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 if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")} {:ok, format_input(comment, "text/plain")}
@ -564,7 +564,7 @@ def validate_character_limit("" = _full_payload, [] = _attachments) do
end end
def validate_character_limit(full_payload, _attachments) do def validate_character_limit(full_payload, _attachments) do
limit = Pleroma.Config.get([:instance, :limit]) limit = Config.get([:instance, :limit])
length = String.length(full_payload) length = String.length(full_payload)
if length <= limit do if length <= limit do

View File

@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI 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 <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else else
{:error, error} -> json_response(conn, :bad_request, %{error: error}) {:error, error} -> json_response(conn, :bad_request, %{error: error})
end 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.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{}) |> 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 = user_params =
[ [
:no_rich_text, :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(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note]) |> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note]) |> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar]) |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
|> Maps.put_if_present(:banner, params[:header]) |> Maps.put_if_present(:banner, params[:header], user_image_value)
|> Maps.put_if_present(:background, params[:pleroma_background_image]) |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present( |> Maps.put_if_present(
:raw_fields, :raw_fields,
params[:fields_attributes], params[:fields_attributes],
@ -347,7 +350,7 @@ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"} {:error, "Can not follow yourself"}
end 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 with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed) render(conn, "relationship.json", user: follower, target: followed)
else else

View File

@ -201,15 +201,13 @@ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
@doc "DELETE /api/v1/statuses/:id" @doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), 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 {: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 else
_e -> {:error, :not_found} _e -> {:error, :not_found}
end end

View File

@ -88,21 +88,20 @@ def direct(%{assigns: %{user: user}} = conn, params) do
) )
end 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 # GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do def public(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local] local_only = params[:local]
cfg_key = if is_nil(user) and restrict_unauthenticated?(local_only) do
if local_only do fail_on_bad_auth(conn)
: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")
else else
activities = activities =
params params
@ -123,6 +122,10 @@ def public(%{assigns: %{user: user}} = conn, params) do
end end
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 defp hashtag_fetching(params, user, local_only) do
tags = tags =
[params[:tag], params[:any]] [params[:tag], params[:any]]
@ -157,15 +160,20 @@ defp hashtag_fetching(params, user, local_only) do
# GET /api/v1/timelines/tag/:tag # GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local] local_only = params[:local]
activities = hashtag_fetching(params, user, local_only)
conn if is_nil(user) and restrict_unauthenticated?(local_only) do
|> add_link_headers(activities, %{"local" => local_only}) fail_on_bad_auth(conn)
|> render("index.json", else
activities: activities, activities = hashtag_fetching(params, user, local_only)
for: user,
as: :activity conn
) |> add_link_headers(activities, %{"local" => local_only})
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
end end
# GET /api/v1/timelines/list/:list_id # GET /api/v1/timelines/list/:list_id

View File

@ -204,6 +204,18 @@ defp do_render("show.json", %{user: user} = opts) do
%{} %{}
end 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), id: to_string(user.id),
username: username_from_nickname(user.nickname), username: username_from_nickname(user.nickname),
@ -246,7 +258,8 @@ defp do_render("show.json", %{user: user} = opts) do
relationship: relationship, relationship: relationship,
skip_thread_containment: user.skip_thread_containment, skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(), 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]) |> maybe_put_role(user, opts[:for])

View File

@ -34,6 +34,8 @@ def render("show.json", _) do
background_upload_limit: Keyword.get(instance, :background_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
background_image: Keyword.get(instance, :background_image), background_image: Keyword.get(instance, :background_image),
chat_limit: Keyword.get(instance, :chat_limit),
description_limit: Keyword.get(instance, :description_limit),
pleroma: %{ pleroma: %{
metadata: %{ metadata: %{
account_activation_required: Keyword.get(instance, :account_activation_required), account_activation_required: Keyword.get(instance, :account_activation_required),

View File

@ -106,7 +106,7 @@ def filename(url_or_path) do
def build_url(sig_base64, url_base64, filename \\ nil) 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", "proxy",
sig_base64, sig_base64,
url_base64, url_base64,

View File

@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify]) 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, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params), {:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do {: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 else
_error -> _error ->
conn conn

View File

@ -5,4 +5,13 @@
defmodule Pleroma.Web.OAuth.MFAView do defmodule Pleroma.Web.OAuth.MFAView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML.Form 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 end

View File

@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.MFAController alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.MFAView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@ -233,9 +235,7 @@ def token_exchange(
with {:ok, app} <- Token.Utils.fetch_app(conn), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do {:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} json(conn, OAuthView.render("token.json", %{user: user, token: token}))
json(conn, Token.Response.build(user, token, response_attrs))
else else
_error -> render_invalid_credentials_error(conn) _error -> render_invalid_credentials_error(conn)
end end
@ -247,9 +247,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
{:ok, auth} <- Authorization.get_by_token(app, fixed_token), {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id), %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} json(conn, OAuthView.render("token.json", %{user: user, token: token}))
json(conn, Token.Response.build(user, token, response_attrs))
else else
error -> error ->
handle_token_exchange_error(conn, error) handle_token_exchange_error(conn, error)
@ -267,7 +265,7 @@ def token_exchange(
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do {: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 else
error -> error ->
handle_token_exchange_error(conn, 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), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do {: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 else
_error -> _error ->
handle_token_exchange_error(conn, :invalid_credentails) 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 defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create_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
end end

View File

@ -5,4 +5,26 @@
defmodule Pleroma.Web.OAuth.OAuthView do defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML.Form 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 end

View File

@ -1,45 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
alias Ecto.Changeset
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -35,17 +34,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] %{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( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
@ -68,56 +56,6 @@ def confirmation_resend(conn, params) do
end end
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" @doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites") render_error(conn, :forbidden, "Can't get favorites")

View File

@ -1,25 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# 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

View File

@ -86,7 +86,10 @@ defp parse_url(url) do
end end
try do 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 html
|> parse_html() |> parse_html()

View File

@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read) 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) get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update) put("/mascot", MascotController, :update)
@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through(:config) 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) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end end

View File

@ -13,9 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.UtilView
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) 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(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 def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick), with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do avatar = User.avatar_url(user) do
@ -89,80 +81,14 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_
end end
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 def frontend_configurations(conn, _params) do
config = config =
Pleroma.Config.get(:frontend_configurations, %{}) Config.get(:frontend_configurations, %{})
|> Enum.into(%{}) |> Enum.into(%{})
json(conn, config) json(conn, config)
end 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>#{version}</version>")
end
def version(conn, _params) do
json(conn, Pleroma.Application.named_version())
end
def emoji(conn, _params) do def emoji(conn, _params) do
emoji = emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->

View File

@ -178,6 +178,7 @@ defp deps do
ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"}, ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
{:telemetry, "~> 0.3"}, {:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"}, {:poolboy, "~> 1.5"},
{:prometheus, "~> 4.6"},
{:prometheus_ex, "~> 3.0"}, {:prometheus_ex, "~> 3.0"},
{:prometheus_plugs, "~> 1.1"}, {:prometheus_plugs, "~> 1.1"},
{:prometheus_phoenix, "~> 1.3"}, {:prometheus_phoenix, "~> 1.3"},

View File

@ -92,7 +92,7 @@
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "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"}, "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"}, "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_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_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"}, "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"},

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-06-19 14:33+0000\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 <srsbzns@cock.li>\n" "Last-Translator: Ben Is <srsbzns@cock.li>\n"
"Language-Team: Italian <https://translate.pleroma.social/projects/pleroma/" "Language-Team: Italian <https://translate.pleroma.social/projects/pleroma/"
"pleroma/it/>\n" "pleroma/it/>\n"
@ -29,258 +29,258 @@ msgstr "non può essere nullo"
## From Ecto.Changeset.unique_constraint/3 ## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken" msgid "has already been taken"
msgstr "" msgstr "è stato già creato"
## From Ecto.Changeset.put_change/3 ## From Ecto.Changeset.put_change/3
msgid "is invalid" msgid "is invalid"
msgstr "" msgstr "non è valido"
## From Ecto.Changeset.validate_format/3 ## From Ecto.Changeset.validate_format/3
msgid "has invalid format" msgid "has invalid format"
msgstr "" msgstr "è in un formato invalido"
## From Ecto.Changeset.validate_subset/3 ## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry" msgid "has an invalid entry"
msgstr "" msgstr "ha una voce invalida"
## From Ecto.Changeset.validate_exclusion/3 ## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved" msgid "is reserved"
msgstr "" msgstr "è vietato"
## From Ecto.Changeset.validate_confirmation/3 ## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation" msgid "does not match confirmation"
msgstr "" msgstr "non corrisponde alla verifica"
## From Ecto.Changeset.no_assoc_constraint/3 ## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry" msgid "is still associated with this entry"
msgstr "" msgstr "è ancora associato con questa voce"
msgid "are still associated with this entry" msgid "are still associated with this entry"
msgstr "" msgstr "sono ancora associati con questa voce"
## From Ecto.Changeset.validate_length/3 ## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)" msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)" msgid_plural "should be %{count} character(s)"
msgstr[0] "" msgstr[0] "dovrebbe essere %{count} carattere"
msgstr[1] "" msgstr[1] "dovrebbero essere %{count} caratteri"
msgid "should have %{count} item(s)" msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)" msgid_plural "should have %{count} item(s)"
msgstr[0] "" msgstr[0] "dovrebbe avere %{count} voce"
msgstr[1] "" msgstr[1] "dovrebbe avere %{count} voci"
msgid "should be at least %{count} character(s)" msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)"
msgstr[0] "" msgstr[0] "dovrebbe contenere almeno %{count} carattere"
msgstr[1] "" msgstr[1] "dovrebbe contenere almeno %{count} caratteri"
msgid "should have at least %{count} item(s)" msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)"
msgstr[0] "" msgstr[0] "dovrebbe avere almeno %{count} voce"
msgstr[1] "" msgstr[1] "dovrebbe avere almeno %{count} voci"
msgid "should be at most %{count} character(s)" msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)"
msgstr[0] "" msgstr[0] "dovrebbe avere al massimo %{count} carattere"
msgstr[1] "" msgstr[1] "dovrebbe avere al massimo %{count} caratteri"
msgid "should have at most %{count} item(s)" msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)" msgid_plural "should have at most %{count} item(s)"
msgstr[0] "" msgstr[0] "dovrebbe avere al massimo %{count} voce"
msgstr[1] "" msgstr[1] "dovrebbe avere al massimo %{count} voci"
## From Ecto.Changeset.validate_number/3 ## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}" msgid "must be less than %{number}"
msgstr "" msgstr "dev'essere minore di %{number}"
msgid "must be greater than %{number}" msgid "must be greater than %{number}"
msgstr "" msgstr "dev'essere maggiore di %{number}"
msgid "must be less than or equal to %{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}" msgid "must be greater than or equal to %{number}"
msgstr "" msgstr "dev'essere maggiore o uguale a %{number}"
msgid "must be equal to %{number}" msgid "must be equal to %{number}"
msgstr "" msgstr "dev'essere uguale a %{number}"
#: lib/pleroma/web/common_api/common_api.ex:421 #: lib/pleroma/web/common_api/common_api.ex:421
#, elixir-format #, elixir-format
msgid "Account not found" msgid "Account not found"
msgstr "" msgstr "Profilo non trovato"
#: lib/pleroma/web/common_api/common_api.ex:249 #: lib/pleroma/web/common_api/common_api.ex:249
#, elixir-format #, elixir-format
msgid "Already voted" msgid "Already voted"
msgstr "" msgstr "Hai già votato"
#: lib/pleroma/web/oauth/oauth_controller.ex:360 #: lib/pleroma/web/oauth/oauth_controller.ex:360
#, elixir-format #, elixir-format
msgid "Bad request" msgid "Bad request"
msgstr "" msgstr "Richiesta invalida"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425
#, elixir-format #, elixir-format
msgid "Can't delete object" msgid "Can't delete object"
msgstr "" msgstr "Non puoi eliminare quest'oggetto"
#: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196 #: lib/pleroma/web/mastodon_api/controllers/status_controller.ex:196
#, elixir-format #, elixir-format
msgid "Can't delete this post" 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:95
#: lib/pleroma/web/controller_helper.ex:101 #: lib/pleroma/web/controller_helper.ex:101
#, elixir-format #, elixir-format
msgid "Can't display this activity" 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:227
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:254
#, elixir-format #, elixir-format
msgid "Can't find user" msgid "Can't find user"
msgstr "" msgstr "Non trovo questo utente"
#: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114 #: lib/pleroma/web/pleroma_api/controllers/account_controller.ex:114
#, elixir-format #, elixir-format
msgid "Can't get favorites" msgid "Can't get favorites"
msgstr "" msgstr "Non posso ricevere i gradimenti"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:437
#, elixir-format #, elixir-format
msgid "Can't like object" msgid "Can't like object"
msgstr "" msgstr "Non posso gradire quest'oggetto"
#: lib/pleroma/web/common_api/utils.ex:556 #: lib/pleroma/web/common_api/utils.ex:556
#, elixir-format #, elixir-format
msgid "Cannot post an empty status without attachments" 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 #: lib/pleroma/web/common_api/utils.ex:504
#, elixir-format #, elixir-format
msgid "Comment must be up to %{max_size} characters" 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 #: lib/pleroma/config/config_db.ex:222
#, elixir-format #, elixir-format
msgid "Config with params %{params} not found" 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 #: lib/pleroma/web/common_api/common_api.ex:95
#, elixir-format #, elixir-format
msgid "Could not delete" msgid "Could not delete"
msgstr "" msgstr "Non eliminato"
#: lib/pleroma/web/common_api/common_api.ex:141 #: lib/pleroma/web/common_api/common_api.ex:141
#, elixir-format #, elixir-format
msgid "Could not favorite" msgid "Could not favorite"
msgstr "" msgstr "Non gradito"
#: lib/pleroma/web/common_api/common_api.ex:370 #: lib/pleroma/web/common_api/common_api.ex:370
#, elixir-format #, elixir-format
msgid "Could not pin" msgid "Could not pin"
msgstr "" msgstr "Non intestato"
#: lib/pleroma/web/common_api/common_api.ex:112 #: lib/pleroma/web/common_api/common_api.ex:112
#, elixir-format #, elixir-format
msgid "Could not repeat" msgid "Could not repeat"
msgstr "" msgstr "Non ripetuto"
#: lib/pleroma/web/common_api/common_api.ex:188 #: lib/pleroma/web/common_api/common_api.ex:188
#, elixir-format #, elixir-format
msgid "Could not unfavorite" msgid "Could not unfavorite"
msgstr "" msgstr "Non sgradito"
#: lib/pleroma/web/common_api/common_api.ex:380 #: lib/pleroma/web/common_api/common_api.ex:380
#, elixir-format #, elixir-format
msgid "Could not unpin" msgid "Could not unpin"
msgstr "" msgstr "Non de-intestato"
#: lib/pleroma/web/common_api/common_api.ex:126 #: lib/pleroma/web/common_api/common_api.ex:126
#, elixir-format #, elixir-format
msgid "Could not unrepeat" 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:428
#: lib/pleroma/web/common_api/common_api.ex:437 #: lib/pleroma/web/common_api/common_api.ex:437
#, elixir-format #, elixir-format
msgid "Could not update state" msgid "Could not update state"
msgstr "" msgstr "Non aggiornato"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
#, elixir-format #, elixir-format
msgid "Error." msgid "Error."
msgstr "" msgstr "Errore."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106 #: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format #, elixir-format
msgid "Invalid CAPTCHA" msgid "Invalid CAPTCHA"
msgstr "" msgstr "CAPTCHA invalido"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117
#: lib/pleroma/web/oauth/oauth_controller.ex:569 #: lib/pleroma/web/oauth/oauth_controller.ex:569
#, elixir-format #, elixir-format
msgid "Invalid credentials" msgid "Invalid credentials"
msgstr "" msgstr "Credenziali invalide"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format #, elixir-format
msgid "Invalid credentials." msgid "Invalid credentials."
msgstr "" msgstr "Credenziali invalide."
#: lib/pleroma/web/common_api/common_api.ex:265 #: lib/pleroma/web/common_api/common_api.ex:265
#, elixir-format #, elixir-format
msgid "Invalid indices" msgid "Invalid indices"
msgstr "" msgstr "Indici invalidi"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 #: lib/pleroma/web/admin_api/admin_api_controller.ex:1147
#, elixir-format #, elixir-format
msgid "Invalid parameters" msgid "Invalid parameters"
msgstr "" msgstr "Parametri invalidi"
#: lib/pleroma/web/common_api/utils.ex:411 #: lib/pleroma/web/common_api/utils.ex:411
#, elixir-format #, elixir-format
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr "Parola d'ordine invalida."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187
#, elixir-format #, elixir-format
msgid "Invalid request" msgid "Invalid request"
msgstr "" msgstr "Richiesta invalida"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109 #: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format #, elixir-format
msgid "Kocaptcha service unavailable" msgid "Kocaptcha service unavailable"
msgstr "" msgstr "Servizio Kocaptcha non disponibile"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113
#, elixir-format #, elixir-format
msgid "Missing parameters" msgid "Missing parameters"
msgstr "" msgstr "Parametri mancanti"
#: lib/pleroma/web/common_api/utils.ex:540 #: lib/pleroma/web/common_api/utils.ex:540
#, elixir-format #, elixir-format
msgid "No such conversation" 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:439
#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 #: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507
#, elixir-format #, elixir-format
msgid "No such permission_group" msgid "No such permission_group"
msgstr "" msgstr "permission_group non esistente"
#: lib/pleroma/plugs/uploaded_media.ex:74 #: 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/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 #: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format #, elixir-format
msgid "Not found" msgid "Not found"
msgstr "" msgstr "Non trovato"
#: lib/pleroma/web/common_api/common_api.ex:241 #: lib/pleroma/web/common_api/common_api.ex:241
#, elixir-format #, elixir-format
msgid "Poll's author can't vote" 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/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49 #: 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 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format #, elixir-format
msgid "Record not found" msgid "Record not found"
msgstr "" msgstr "Voce non trovata"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 #: 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/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 #: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#, elixir-format #, elixir-format
msgid "Something went wrong" msgid "Something went wrong"
msgstr "" msgstr "C'è stato un problema"
#: lib/pleroma/web/common_api/activity_draft.ex:107 #: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format #, elixir-format
msgid "The message visibility must be direct" msgid "The message visibility must be direct"
msgstr "" msgstr "Il messaggio dev'essere privato"
#: lib/pleroma/web/common_api/utils.ex:566 #: lib/pleroma/web/common_api/utils.ex:566
#, elixir-format #, elixir-format
msgid "The status is over the character limit" 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 #: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format #, elixir-format
msgid "This resource requires authentication." msgid "This resource requires authentication."
msgstr "" msgstr "Accedi per leggere."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format #, elixir-format
msgid "Throttled" msgid "Throttled"
msgstr "" msgstr "Strozzato"
#: lib/pleroma/web/common_api/common_api.ex:266 #: lib/pleroma/web/common_api/common_api.ex:266
#, elixir-format #, elixir-format
msgid "Too many choices" msgid "Too many choices"
msgstr "" msgstr "Troppe alternative"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442
#, elixir-format #, elixir-format
msgid "Unhandled activity type" msgid "Unhandled activity type"
msgstr "" msgstr "Tipo di attività non gestibile"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:536 #: lib/pleroma/web/admin_api/admin_api_controller.ex:536
#, elixir-format #, elixir-format
msgid "You can't revoke your own admin status." 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:218
#: lib/pleroma/web/oauth/oauth_controller.ex:309 #: lib/pleroma/web/oauth/oauth_controller.ex:309
#, elixir-format #, elixir-format
msgid "Your account is currently disabled" 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:180
#: lib/pleroma/web/oauth/oauth_controller.ex:332 #: lib/pleroma/web/oauth/oauth_controller.ex:332
#, elixir-format #, elixir-format
msgid "Your login is missing a confirmed e-mail address" 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 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389
#, elixir-format #, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}" 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 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472
#, elixir-format #, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}" 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 #: lib/pleroma/web/common_api/common_api.ex:388
#, elixir-format #, elixir-format
msgid "conversation is already muted" 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:316
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
#, elixir-format #, elixir-format
msgid "error" msgid "error"
msgstr "" msgstr "errore"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 #: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29
#, elixir-format #, elixir-format
msgid "mascots can only be images" msgid "mascots can only be images"
msgstr "" msgstr "le mascotte possono solo essere immagini"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60
#, elixir-format #, elixir-format
msgid "not found" msgid "not found"
msgstr "" msgstr "non trovato"
#: lib/pleroma/web/oauth/oauth_controller.ex:395 #: lib/pleroma/web/oauth/oauth_controller.ex:395
#, elixir-format #, elixir-format
msgid "Bad OAuth request." msgid "Bad OAuth request."
msgstr "" msgstr "Richiesta OAuth malformata."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115 #: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format #, elixir-format
msgid "CAPTCHA already used" msgid "CAPTCHA already used"
msgstr "" msgstr "CAPTCHA già utilizzato"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112 #: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format #, elixir-format
msgid "CAPTCHA expired" msgid "CAPTCHA expired"
msgstr "" msgstr "CAPTCHA scaduto"
#: lib/pleroma/plugs/uploaded_media.ex:55 #: lib/pleroma/plugs/uploaded_media.ex:55
#, elixir-format #, elixir-format
msgid "Failed" msgid "Failed"
msgstr "" msgstr "Fallito"
#: lib/pleroma/web/oauth/oauth_controller.ex:411 #: lib/pleroma/web/oauth/oauth_controller.ex:411
#, elixir-format #, elixir-format
msgid "Failed to authenticate: %{message}." msgid "Failed to authenticate: %{message}."
msgstr "" msgstr "Autenticazione fallita per: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:442 #: lib/pleroma/web/oauth/oauth_controller.ex:442
#, elixir-format #, elixir-format
msgid "Failed to set up user account." msgid "Failed to set up user account."
msgstr "" msgstr "Profilo utente non creato."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38 #: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format #, elixir-format
msgid "Insufficient permissions: %{permissions}." msgid "Insufficient permissions: %{permissions}."
msgstr "" msgstr "Permessi insufficienti: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:94 #: lib/pleroma/plugs/uploaded_media.ex:94
#, elixir-format #, elixir-format
msgid "Internal Error" msgid "Internal Error"
msgstr "" msgstr "Errore interno"
#: lib/pleroma/web/oauth/fallback_controller.ex:22 #: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29 #: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format #, elixir-format
msgid "Invalid Username/Password" msgid "Invalid Username/Password"
msgstr "" msgstr "Nome utente/parola d'ordine invalidi"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118 #: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format #, elixir-format
msgid "Invalid answer data" msgid "Invalid answer data"
msgstr "" msgstr "Risposta malformata"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128
#, elixir-format #, elixir-format
msgid "Nodeinfo schema version not handled" msgid "Nodeinfo schema version not handled"
msgstr "" msgstr "Versione schema nodeinfo non compatibile"
#: lib/pleroma/web/oauth/oauth_controller.ex:169 #: lib/pleroma/web/oauth/oauth_controller.ex:169
#, elixir-format #, elixir-format
msgid "This action is outside the authorized scopes" 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 #: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format #, elixir-format
msgid "Unknown error, please check the details and try again." 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:116
#: lib/pleroma/web/oauth/oauth_controller.ex:155 #: lib/pleroma/web/oauth/oauth_controller.ex:155
#, elixir-format #, elixir-format
msgid "Unlisted redirect_uri." msgid "Unlisted redirect_uri."
msgstr "" msgstr "redirect_uri nascosto."
#: lib/pleroma/web/oauth/oauth_controller.ex:391 #: lib/pleroma/web/oauth/oauth_controller.ex:391
#, elixir-format #, elixir-format
msgid "Unsupported OAuth provider: %{provider}." msgid "Unsupported OAuth provider: %{provider}."
msgstr "" msgstr "Gestore OAuth non supportato: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72 #: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format #, elixir-format
msgid "Uploader callback timeout" msgid "Uploader callback timeout"
msgstr "" msgstr "Callback caricatmento scaduta"
#: lib/pleroma/web/uploader_controller.ex:23 #: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format #, elixir-format
msgid "bad request" msgid "bad request"
msgstr "" msgstr "richiesta malformata"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103 #: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format #, elixir-format
msgid "CAPTCHA Error" msgid "CAPTCHA Error"
msgstr "" msgstr "Errore CAPTCHA"
#: lib/pleroma/web/common_api/common_api.ex:200 #: lib/pleroma/web/common_api/common_api.ex:200
#, elixir-format #, elixir-format
msgid "Could not add reaction emoji" msgid "Could not add reaction emoji"
msgstr "" msgstr "Reazione emoji non riuscita"
#: lib/pleroma/web/common_api/common_api.ex:211 #: lib/pleroma/web/common_api/common_api.ex:211
#, elixir-format #, elixir-format
msgid "Could not remove reaction emoji" msgid "Could not remove reaction emoji"
msgstr "" msgstr "Rimozione reazione non riuscita"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129 #: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format #, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})" msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr "" msgstr "CAPTCHA invalido (Parametro mancante: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 #: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format #, elixir-format
msgid "List not found" msgid "List not found"
msgstr "" msgstr "Lista non trovata"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124
#, elixir-format #, elixir-format
msgid "Missing parameter: %{name}" msgid "Missing parameter: %{name}"
msgstr "" msgstr "Parametro mancante: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:207 #: lib/pleroma/web/oauth/oauth_controller.ex:207
#: lib/pleroma/web/oauth/oauth_controller.ex:322 #: lib/pleroma/web/oauth/oauth_controller.ex:322
#, elixir-format #, elixir-format
msgid "Password reset is required" msgid "Password reset is required"
msgstr "" msgstr "Necessario reimpostare parola d'ordine"
#: lib/pleroma/tests/auth_test_controller.ex:9 #: 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 #: 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 #, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr "" msgstr ""
"Sicurezza violata: il controllo autorizzazioni di OAuth non è stato svolto "
"né saltato."
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format #, elixir-format
msgid "Two-factor authentication enabled, you must use a access token." msgid "Two-factor authentication enabled, you must use a access token."
msgstr "" msgstr ""
"Autenticazione bifattoriale abilitata, devi utilizzare una chiave d'accesso."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while adding file to pack." 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 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while creating pack." 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 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while removing file from pack." 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 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while updating file in pack." 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 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while updating pack metadata." 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 #: lib/pleroma/plugs/user_is_admin_plug.ex:40
#, elixir-format #, elixir-format
msgid "User is not an admin or OAuth admin scope is not granted." msgid "User is not an admin or OAuth admin scope is not granted."
msgstr "" msgstr ""
"L'utente non è un amministratore o non ha ricevuto questa autorizzazione "
"OAuth."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format #, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance" 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 #: lib/pleroma/web/admin_api/admin_api_controller.ex:502
#, elixir-format #, elixir-format
msgid "You can't revoke your own admin/moderator status." 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 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105
#, elixir-format #, elixir-format
msgid "authorization required for timeline view" msgid "authorization required for timeline view"
msgstr "" msgstr "autorizzazione richiesta per vedere la sequenza"

View File

@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-13 16:37+0000\n" "POT-Creation-Date: 2020-05-13 16:37+0000\n"
"PO-Revision-Date: 2020-05-16 17:13+0000\n" "PO-Revision-Date: 2020-07-09 14:40+0000\n"
"Last-Translator: Jędrzej Tomaszewski <jederow@hotmail.com>\n" "Last-Translator: Ben Is <srsbzns@cock.li>\n"
"Language-Team: Polish <https://translate.pleroma.social/projects/pleroma/" "Language-Team: Polish <https://translate.pleroma.social/projects/pleroma/"
"pleroma/pl/>\n" "pleroma/pl/>\n"
"Language: pl\n" "Language: pl\n"
@ -50,7 +50,7 @@ msgstr "jest zarezerwowany"
## From Ecto.Changeset.validate_confirmation/3 ## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation" msgid "does not match confirmation"
msgstr "" msgstr "nie pasuje do potwierdzenia"
## From Ecto.Changeset.no_assoc_constraint/3 ## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry" msgid "is still associated with this entry"

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.18fea621d430000acc27.css rel=stylesheet><link href=/static/css/app.613cef07981cd95ccceb.css rel=stylesheet><link href=/static/fontello.1589385935077.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.561a1c605d1dfb0e6f74.js></script><script type=text/javascript src=/static/js/app.838ffa9aecf210c7d744.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/app.77b1644622e3bae24b6b.css rel=stylesheet><link href=/static/fontello.1594374054351.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.247dc52c7abe6a0dab87.js></script><script type=text/javascript src=/static/js/app.1e68e208590653dab5aa.js></script></body></html>

Binary file not shown.

View File

@ -14,7 +14,6 @@
"logoMargin": ".1em", "logoMargin": ".1em",
"logoMask": true, "logoMask": true,
"minimalScopesMode": false, "minimalScopesMode": false,
"noAttachmentLinks": false,
"nsfwCensorImage": "", "nsfwCensorImage": "",
"postContentType": "text/plain", "postContentType": "text/plain",
"redirectRootLogin": "/main/friends", "redirectRootLogin": "/main/friends",
@ -22,6 +21,7 @@
"scopeCopy": true, "scopeCopy": true,
"showFeaturesPanel": true, "showFeaturesPanel": true,
"showInstanceSpecificPanel": false, "showInstanceSpecificPanel": false,
"sidebarRight": false,
"subjectLineBehavior": "email", "subjectLineBehavior": "email",
"theme": "pleroma-dark", "theme": "pleroma-dark",
"webPushNotifications": false "webPushNotifications": false

Binary file not shown.

View File

@ -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":""}

File diff suppressed because one or more lines are too long

View File

@ -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":""}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -80,8 +80,18 @@
<glyph glyph-name="user" unicode="&#xe824;" d="M714 76q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" /> <glyph glyph-name="user" unicode="&#xe824;" d="M714 76q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
<glyph glyph-name="download" unicode="&#xe825;" d="M714 107q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
<glyph glyph-name="bookmark" unicode="&#xe826;" d="M650 786q12 0 24-5 19-8 29-23t11-35v-719q0-19-11-35t-29-23q-10-4-24-4-27 0-47 18l-246 236-246-236q-20-19-46-19-13 0-25 5-18 7-29 23t-11 35v719q0 19 11 35t29 23q12 5 25 5h585z" horiz-adv-x="714.3" />
<glyph glyph-name="ok" unicode="&#xe827;" d="M933 541q0-22-16-38l-404-404-76-76q-16-15-38-15t-38 15l-76 76-202 202q-15 16-15 38t15 38l76 76q16 16 38 16t38-16l164-165 366 367q16 16 38 16t38-16l76-76q16-15 16-38z" horiz-adv-x="1000" /> <glyph glyph-name="ok" unicode="&#xe827;" d="M933 541q0-22-16-38l-404-404-76-76q-16-15-38-15t-38 15l-76 76-202 202q-15 16-15 38t15 38l76 76q16 16 38 16t38-16l164-165 366 367q16 16 38 16t38-16l76-76q16-15 16-38z" horiz-adv-x="1000" />
<glyph glyph-name="music" unicode="&#xe828;" d="M857 732v-625q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 33 58 18 54 6q58 0 107-22v300l-429-132v-396q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 34 58 17 54 6q58 0 107-21v539q0 17 10 32t28 20l464 142q7 3 16 3 22 0 38-16t15-38z" horiz-adv-x="857.1" />
<glyph glyph-name="doc" unicode="&#xe829;" d="M819 645q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z" horiz-adv-x="857.1" />
<glyph glyph-name="block" unicode="&#xe82a;" d="M732 359q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> <glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
@ -90,6 +100,10 @@
<glyph glyph-name="link-ext-alt" unicode="&#xf08f;" d="M714 339v268q0 15-10 25t-25 11h-268q-24 0-33-22-10-23 8-39l80-80-298-298q-11-11-11-26t11-25l57-57q11-10 25-10t25 10l298 298 81-80q10-11 25-11 6 0 14 3 21 10 21 33z m143 286v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" /> <glyph glyph-name="link-ext-alt" unicode="&#xf08f;" d="M714 339v268q0 15-10 25t-25 11h-268q-24 0-33-22-10-23 8-39l80-80-298-298q-11-11-11-26t11-25l57-57q11-10 25-10t25 10l298 298 81-80q10-11 25-11 6 0 14 3 21 10 21 33z m143 286v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
<glyph glyph-name="bookmark-empty" unicode="&#xf097;" d="M643 714h-572v-693l237 227 49 47 50-47 236-227v693z m7 72q12 0 24-5 19-8 29-23t11-35v-719q0-19-11-35t-29-23q-10-4-24-4-27 0-47 18l-246 236-246-236q-20-19-46-19-13 0-25 5-18 7-29 23t-11 35v719q0 19 11 35t29 23q12 5 25 5h585z" horiz-adv-x="714.3" />
<glyph glyph-name="filter" unicode="&#xf0b0;" d="M783 692q9-22-8-39l-275-275v-414q0-23-22-33-7-3-14-3-15 0-25 11l-143 143q-10 11-10 25v271l-275 275q-18 17-8 39 9 22 33 22h714q23 0 33-22z" horiz-adv-x="785.7" />
<glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 107v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" /> <glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 107v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
<glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" /> <glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -363,6 +363,48 @@
"css": "ok", "css": "ok",
"code": 59431, "code": 59431,
"src": "fontawesome" "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"
} }
] ]
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More