forked from AkkomaGang/akkoma
Merge branch 'develop' into fix/csp-for-captcha
This commit is contained in:
commit
49c4e24953
184 changed files with 2364 additions and 1565 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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>
|
||||||
|
@ -51,6 +55,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>
|
||||||
|
@ -58,8 +63,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
|
||||||
|
@ -73,6 +80,7 @@ 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
|
||||||
- Fix CSP policy generation to include remote Captcha services
|
- Fix CSP policy generation to include remote Captcha services
|
||||||
|
|
||||||
## [Unreleased (patch)]
|
## [Unreleased (patch)]
|
||||||
|
@ -215,7 +223,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.
|
||||||
|
|
|
@ -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:")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -71,6 +71,7 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||||
- `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.
|
||||||
|
- `favicon`: nullable URL string, Favicon image of the user's instance
|
||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
|
@ -182,10 +183,12 @@ 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -220,6 +223,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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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 instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
|
* `description`: The instance’s 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 Pleroma’s Relay, which makes it possible to follow a whole instance.
|
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
|
||||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
|
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. See also: `restrict_unauthenticated`.
|
||||||
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
|
* `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
|
||||||
|
|
|
@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do
|
||||||
:swarm
|
:swarm
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@reject_groups [
|
||||||
|
:postgrex,
|
||||||
|
:tesla
|
||||||
|
]
|
||||||
|
|
||||||
if Code.ensure_loaded?(Config.Reader) do
|
if Code.ensure_loaded?(Config.Reader) do
|
||||||
@reader Config.Reader
|
@reader Config.Reader
|
||||||
|
|
||||||
|
@ -47,7 +52,8 @@ defp filter(configs) do
|
||||||
@spec filter_group(atom(), keyword()) :: keyword()
|
@spec filter_group(atom(), keyword()) :: keyword()
|
||||||
def filter_group(group, configs) do
|
def filter_group(group, configs) do
|
||||||
Enum.reject(configs[group], fn {key, _v} ->
|
Enum.reject(configs[group], fn {key, _v} ->
|
||||||
key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
|
key in @reject_keys or group in @reject_groups or
|
||||||
|
(group == :phoenix and key == :serve_endpoints)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)}"
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -539,15 +539,12 @@ 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
|
else
|
||||||
_ -> changeset
|
_ -> changeset
|
||||||
end
|
end
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp put_upload(value, type) do
|
defp put_upload(value, type) do
|
||||||
|
@ -1546,7 +1543,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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -233,8 +233,10 @@ 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
|
||||||
|
|
||||||
|
if href do
|
||||||
attachment_url =
|
attachment_url =
|
||||||
%{"href" => href}
|
%{"href" => href}
|
||||||
|> Maps.put_if_present("mediaType", media_type)
|
|> Maps.put_if_present("mediaType", media_type)
|
||||||
|
@ -244,7 +246,11 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|
||||||
|> 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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -102,6 +102,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
||||||
type: :object,
|
type: :object,
|
||||||
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`"
|
||||||
|
},
|
||||||
|
favicon: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
nullable: true,
|
||||||
|
description: "Favicon image of the user's instance"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -101,12 +101,16 @@ 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
|
||||||
|
if activity.data["state"] == "reject" do
|
||||||
|
{:error, :rejected}
|
||||||
|
else
|
||||||
{:ok, follower, followed, activity}
|
{:ok, follower, followed, activity}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def unfollow(follower, unfollowed) do
|
def unfollow(follower, unfollowed) do
|
||||||
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -168,9 +171,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],
|
||||||
|
@ -346,7 +349,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
|
||||||
|
|
|
@ -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 <-
|
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||||
try_render(conn, "show.json",
|
try_render(conn, "show.json",
|
||||||
activity: activity,
|
activity: activity,
|
||||||
for: user,
|
for: user,
|
||||||
with_direct_conversation_id: true,
|
with_direct_conversation_id: true,
|
||||||
with_source: true
|
with_source: true
|
||||||
),
|
)
|
||||||
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
|
||||||
render
|
|
||||||
else
|
else
|
||||||
_e -> {:error, :not_found}
|
_e -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
|
|
|
@ -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,6 +160,10 @@ 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]
|
||||||
|
|
||||||
|
if is_nil(user) and restrict_unauthenticated?(local_only) do
|
||||||
|
fail_on_bad_auth(conn)
|
||||||
|
else
|
||||||
activities = hashtag_fetching(params, user, local_only)
|
activities = hashtag_fetching(params, user, local_only)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
@ -167,6 +174,7 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do
|
||||||
as: :activity
|
as: :activity
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# GET /api/v1/timelines/list/:list_id
|
# GET /api/v1/timelines/list/:list_id
|
||||||
def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
|
def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
|
||||||
|
|
|
@ -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),
|
||||||
|
@ -245,7 +257,8 @@ defp do_render("show.json", %{user: user} = opts) do
|
||||||
hide_favorites: user.hide_favorites,
|
hide_favorites: user.hide_favorites,
|
||||||
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(),
|
||||||
|
favicon: favicon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> maybe_put_role(user, opts[:for])
|
|> maybe_put_role(user, opts[:for])
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,9 @@ def stream(topics, items) do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_by_user?(%User{} = user, %Activity{} = item) do
|
def filtered_by_user?(user, item, streamed_type \\ :activity)
|
||||||
|
|
||||||
|
def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
|
||||||
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
|
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
|
||||||
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
|
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
|
||||||
|
|
||||||
|
@ -116,7 +118,9 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do
|
||||||
true <-
|
true <-
|
||||||
Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
|
Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
|
||||||
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
|
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
|
||||||
true <- !(item.data["type"] == "Announce" && parent.data["actor"] == user.ap_id),
|
true <-
|
||||||
|
!(streamed_type == :activity && item.data["type"] == "Announce" &&
|
||||||
|
parent.data["actor"] == user.ap_id),
|
||||||
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
|
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
|
||||||
true <- MapSet.disjoint?(recipients, recipient_blocks),
|
true <- MapSet.disjoint?(recipients, recipient_blocks),
|
||||||
%{host: item_host} <- URI.parse(item.actor),
|
%{host: item_host} <- URI.parse(item.actor),
|
||||||
|
@ -131,8 +135,8 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do
|
def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do
|
||||||
filtered_by_user?(user, activity)
|
filtered_by_user?(user, activity, :notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_stream("direct", item) do
|
defp do_stream("direct", item) do
|
||||||
|
|
|
@ -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,60 +81,6 @@ 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, %{})
|
Pleroma.Config.get(:frontend_configurations, %{})
|
||||||
|
@ -151,18 +89,6 @@ def frontend_configurations(conn, _params) do
|
||||||
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 ->
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.RemoveTeslaFromConfig do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("DELETE FROM config WHERE config.group = ':tesla'")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -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.493b9b5acee37ba97824.css rel=stylesheet><link href=/static/fontello.1594134783339.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.8837fb59589d1dd6acda.js></script><script type=text/javascript src=/static/js/app.53001fa190f37cf2743e.js></script></body></html>
|
|
@ -80,6 +80,7 @@ .display-name {
|
||||||
/* keep emoji from being hilariously huge */
|
/* keep emoji from being hilariously huge */
|
||||||
.display-name img {
|
.display-name img {
|
||||||
max-height: 1em;
|
max-height: 1em;
|
||||||
|
max-width: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name .nickname {
|
.display-name .nickname {
|
||||||
|
|
|
@ -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
|
||||||
|
|
BIN
priv/static/static/css/2.0778a6a864a1307a6c41.css
Normal file
BIN
priv/static/static/css/2.0778a6a864a1307a6c41.css
Normal file
Binary file not shown.
1
priv/static/static/css/2.0778a6a864a1307a6c41.css.map
Normal file
1
priv/static/static/css/2.0778a6a864a1307a6c41.css.map
Normal 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":""}
|
Binary file not shown.
1
priv/static/static/css/3.b2603a50868c68a1c192.css.map
Normal file
1
priv/static/static/css/3.b2603a50868c68a1c192.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
priv/static/static/css/app.493b9b5acee37ba97824.css
Normal file
BIN
priv/static/static/css/app.493b9b5acee37ba97824.css
Normal file
Binary file not shown.
1
priv/static/static/css/app.493b9b5acee37ba97824.css.map
Normal file
1
priv/static/static/css/app.493b9b5acee37ba97824.css.map
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -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":""}
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -80,8 +80,16 @@
|
||||||
|
|
||||||
<glyph glyph-name="user" unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="spin3" unicode="" 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="" 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="" 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="" 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 +98,10 @@
|
||||||
|
|
||||||
<glyph glyph-name="link-ext-alt" unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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.
BIN
priv/static/static/font/fontello.1594134783339.woff
Normal file
BIN
priv/static/static/font/fontello.1594134783339.woff
Normal file
Binary file not shown.
BIN
priv/static/static/font/fontello.1594134783339.woff2
Normal file
BIN
priv/static/static/font/fontello.1594134783339.woff2
Normal file
Binary file not shown.
BIN
priv/static/static/fontello.1594030805019.css
vendored
Normal file
BIN
priv/static/static/fontello.1594030805019.css
vendored
Normal file
Binary file not shown.
BIN
priv/static/static/fontello.1594134783339.css
vendored
Normal file
BIN
priv/static/static/fontello.1594134783339.css
vendored
Normal file
Binary file not shown.
|
@ -363,6 +363,42 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
BIN
priv/static/static/js/10.4a22c77e34edcd678d2f.js
Normal file
BIN
priv/static/static/js/10.4a22c77e34edcd678d2f.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/10.4a22c77e34edcd678d2f.js.map
Normal file
BIN
priv/static/static/js/10.4a22c77e34edcd678d2f.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/11.787aa24e4fd5caef9adb.js
Normal file
BIN
priv/static/static/js/11.787aa24e4fd5caef9adb.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/11.787aa24e4fd5caef9adb.js.map
Normal file
BIN
priv/static/static/js/11.787aa24e4fd5caef9adb.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/12.35a510cf14233f0c6e1f.js
Normal file
BIN
priv/static/static/js/12.35a510cf14233f0c6e1f.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/12.35a510cf14233f0c6e1f.js.map
Normal file
BIN
priv/static/static/js/12.35a510cf14233f0c6e1f.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/13.7931a609d62a42678085.js
Normal file
BIN
priv/static/static/js/13.7931a609d62a42678085.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/13.7931a609d62a42678085.js.map
Normal file
BIN
priv/static/static/js/13.7931a609d62a42678085.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/14.cc092634462fd2a4cfbc.js
Normal file
BIN
priv/static/static/js/14.cc092634462fd2a4cfbc.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/14.cc092634462fd2a4cfbc.js.map
Normal file
BIN
priv/static/static/js/14.cc092634462fd2a4cfbc.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/15.e9ddc5dfd38426398e00.js
Normal file
BIN
priv/static/static/js/15.e9ddc5dfd38426398e00.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/15.e9ddc5dfd38426398e00.js.map
Normal file
BIN
priv/static/static/js/15.e9ddc5dfd38426398e00.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/16.476e7809b8593264469e.js
Normal file
BIN
priv/static/static/js/16.476e7809b8593264469e.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/16.476e7809b8593264469e.js.map
Normal file
BIN
priv/static/static/js/16.476e7809b8593264469e.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/17.acbe4c09f05ae56c76a2.js
Normal file
BIN
priv/static/static/js/17.acbe4c09f05ae56c76a2.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/17.acbe4c09f05ae56c76a2.js.map
Normal file
BIN
priv/static/static/js/17.acbe4c09f05ae56c76a2.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/18.a8ccd7f2a47c5c94b3b9.js
Normal file
BIN
priv/static/static/js/18.a8ccd7f2a47c5c94b3b9.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/18.a8ccd7f2a47c5c94b3b9.js.map
Normal file
BIN
priv/static/static/js/18.a8ccd7f2a47c5c94b3b9.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/19.5894e9c12b4fd5e45872.js
Normal file
BIN
priv/static/static/js/19.5894e9c12b4fd5e45872.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/19.5894e9c12b4fd5e45872.js.map
Normal file
BIN
priv/static/static/js/19.5894e9c12b4fd5e45872.js.map
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
priv/static/static/js/2.f8dee9318a6f84ea92c3.js
Normal file
BIN
priv/static/static/js/2.f8dee9318a6f84ea92c3.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/2.f8dee9318a6f84ea92c3.js.map
Normal file
BIN
priv/static/static/js/2.f8dee9318a6f84ea92c3.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/20.43b5b27b0f68474f3b72.js
Normal file
BIN
priv/static/static/js/20.43b5b27b0f68474f3b72.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/20.43b5b27b0f68474f3b72.js.map
Normal file
BIN
priv/static/static/js/20.43b5b27b0f68474f3b72.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/21.72b45b01be9d0f4c62ce.js
Normal file
BIN
priv/static/static/js/21.72b45b01be9d0f4c62ce.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/21.72b45b01be9d0f4c62ce.js.map
Normal file
BIN
priv/static/static/js/21.72b45b01be9d0f4c62ce.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/22.26f13a22ad57a0d14670.js
Normal file
BIN
priv/static/static/js/22.26f13a22ad57a0d14670.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/22.26f13a22ad57a0d14670.js.map
Normal file
BIN
priv/static/static/js/22.26f13a22ad57a0d14670.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/23.91a60b775352a806f887.js
Normal file
BIN
priv/static/static/js/23.91a60b775352a806f887.js
Normal file
Binary file not shown.
BIN
priv/static/static/js/23.91a60b775352a806f887.js.map
Normal file
BIN
priv/static/static/js/23.91a60b775352a806f887.js.map
Normal file
Binary file not shown.
BIN
priv/static/static/js/24.c8d8438aac954d4707ac.js
Normal file
BIN
priv/static/static/js/24.c8d8438aac954d4707ac.js
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue