mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-03-13 07:02:41 +00:00
Merge remote-tracking branch 'origin/develop' into post-languages
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
7620b520c1
133 changed files with 3248 additions and 1012 deletions
|
@ -26,10 +26,10 @@ cache: &global_cache_policy
|
|||
- _build
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
- build
|
||||
- lint
|
||||
- test
|
||||
- check-changelog
|
||||
- benchmark
|
||||
- deploy
|
||||
- release
|
||||
|
@ -113,7 +113,7 @@ benchmark:
|
|||
variables:
|
||||
MIX_ENV: benchmark
|
||||
services:
|
||||
- name: postgres:9.6-alpine
|
||||
- name: postgres:11.22-alpine
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
|
@ -169,25 +169,6 @@ unit-testing-1.12-erratic:
|
|||
- mix ecto.migrate
|
||||
- mix test --only=erratic
|
||||
|
||||
unit-testing-1.12-rum:
|
||||
extends:
|
||||
- .build_changes_policy
|
||||
- .using-ci-base
|
||||
stage: test
|
||||
cache: *testing_cache_policy
|
||||
services:
|
||||
- name: git.pleroma.social:5050/pleroma/pleroma/postgres-with-rum-13
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
variables:
|
||||
<<: *global_variables
|
||||
RUM_ENABLED: "true"
|
||||
script:
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
|
||||
- mix test --preload-modules
|
||||
|
||||
formatting-1.13:
|
||||
extends: .build_changes_policy
|
||||
image: &formatting_elixir elixir:1.13-alpine
|
||||
|
|
1
changelog.d/backups-follows.add
Normal file
1
changelog.d/backups-follows.add
Normal file
|
@ -0,0 +1 @@
|
|||
Include following/followers in backups
|
1
changelog.d/bookmark-folders.add
Normal file
1
changelog.d/bookmark-folders.add
Normal file
|
@ -0,0 +1 @@
|
|||
Allow to group bookmarks in folders
|
0
changelog.d/bookmark-folders.skip
Normal file
0
changelog.d/bookmark-folders.skip
Normal file
1
changelog.d/card-endpoint.remove
Normal file
1
changelog.d/card-endpoint.remove
Normal file
|
@ -0,0 +1 @@
|
|||
Mastodon API: Remove deprecated GET /api/v1/statuses/:id/card endpoint https://github.com/mastodon/mastodon/pull/11213
|
1
changelog.d/card-image-description.add
Normal file
1
changelog.d/card-image-description.add
Normal file
|
@ -0,0 +1 @@
|
|||
Include image description in status media cards
|
0
changelog.d/description-meilisearch-type.skip
Normal file
0
changelog.d/description-meilisearch-type.skip
Normal file
1
changelog.d/fep-2c59.add
Normal file
1
changelog.d/fep-2c59.add
Normal file
|
@ -0,0 +1 @@
|
|||
Implement FEP-2c59, add "webfinger" to user actor
|
1
changelog.d/ffmpeg-limiter.add
Normal file
1
changelog.d/ffmpeg-limiter.add
Normal file
|
@ -0,0 +1 @@
|
|||
Framegrabs with ffmpeg will execute with a 5 second timeout and cache the URLs of failures with a TTL of 15 minutes to prevent excessive retries.
|
0
changelog.d/fix-bookmark-folder-tests.skip
Normal file
0
changelog.d/fix-bookmark-folder-tests.skip
Normal file
1
changelog.d/force-mention-mrf.add
Normal file
1
changelog.d/force-mention-mrf.add
Normal file
|
@ -0,0 +1 @@
|
|||
Add ForceMention MRF
|
1
changelog.d/framegrabs.fix
Normal file
1
changelog.d/framegrabs.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Video framegrabs were not working correctly after the change to use Exile to execute ffmpeg
|
1
changelog.d/instance-contact-account.add
Normal file
1
changelog.d/instance-contact-account.add
Normal file
|
@ -0,0 +1 @@
|
|||
Add contact account to InstanceView
|
1
changelog.d/instance-rules.add
Normal file
1
changelog.d/instance-rules.add
Normal file
|
@ -0,0 +1 @@
|
|||
Add instance rules
|
1
changelog.d/issue-3241.fix
Normal file
1
changelog.d/issue-3241.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Handle cases when users.inbox is nil.
|
1
changelog.d/link-verification.add
Normal file
1
changelog.d/link-verification.add
Normal file
|
@ -0,0 +1 @@
|
|||
Verify profile link ownership with rel="me"
|
1
changelog.d/mastodon_api_v2.add
Normal file
1
changelog.d/mastodon_api_v2.add
Normal file
|
@ -0,0 +1 @@
|
|||
Add new parameters to /api/v2/instance: configuration[accounts][max_pinned_statuses] and configuration[statuses][characters_reserved_per_url]
|
1
changelog.d/missing-mrfs.add
Normal file
1
changelog.d/missing-mrfs.add
Normal file
|
@ -0,0 +1 @@
|
|||
Startup detection for configured MRF modules that are missing or incorrectly defined
|
1
changelog.d/notifications.fix
Normal file
1
changelog.d/notifications.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Notifications: improve performance by filtering on users table instead of activities table
|
1
changelog.d/postgres-jit.change
Normal file
1
changelog.d/postgres-jit.change
Normal file
|
@ -0,0 +1 @@
|
|||
Disable jit by default for PostgreSQL
|
1
changelog.d/public-polls.add
Normal file
1
changelog.d/public-polls.add
Normal file
|
@ -0,0 +1 @@
|
|||
Expose nonAnonymous field from Smithereen polls
|
1
changelog.d/receiverworker-error-handling.fix
Normal file
1
changelog.d/receiverworker-error-handling.fix
Normal file
|
@ -0,0 +1 @@
|
|||
ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …}
|
1
changelog.d/rich_media_refactor.change
Normal file
1
changelog.d/rich_media_refactor.change
Normal file
|
@ -0,0 +1 @@
|
|||
Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.
|
0
changelog.d/test-improvements.skip
Normal file
0
changelog.d/test-improvements.skip
Normal file
1
changelog.d/transient-validators-defaults.change
Normal file
1
changelog.d/transient-validators-defaults.change
Normal file
|
@ -0,0 +1 @@
|
|||
Set default values on validators for transient objects (attachment, poll options)
|
1
changelog.d/web_push_filtered.fix
Normal file
1
changelog.d/web_push_filtered.fix
Normal file
|
@ -0,0 +1 @@
|
|||
Web Push notifications are no longer generated for muted/blocked threads and users.
|
|
@ -415,6 +415,10 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil
|
|||
|
||||
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
|
||||
|
||||
config :pleroma, :mrf_force_mention,
|
||||
mention_parent: true,
|
||||
mention_quoted: true
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
enabled: true,
|
||||
ignore_hosts: [],
|
||||
|
@ -424,7 +428,11 @@ config :pleroma, :rich_media,
|
|||
Pleroma.Web.RichMedia.Parsers.OEmbed
|
||||
],
|
||||
failure_backoff: 60_000,
|
||||
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
|
||||
ttl_setters: [
|
||||
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl,
|
||||
Pleroma.Web.RichMedia.Parser.TTL.Opengraph
|
||||
],
|
||||
max_body: 5_000_000
|
||||
|
||||
config :pleroma, :media_proxy,
|
||||
enabled: false,
|
||||
|
@ -571,7 +579,8 @@ config :pleroma, Oban,
|
|||
attachments_cleanup: 1,
|
||||
new_users_digest: 1,
|
||||
mute_expire: 5,
|
||||
search_indexing: 10
|
||||
search_indexing: 10,
|
||||
rich_media_expiration: 2
|
||||
],
|
||||
plugins: [Oban.Plugins.Pruner],
|
||||
crontab: [
|
||||
|
@ -795,7 +804,7 @@ config :pleroma, :modules, runtime_dir: "instance/modules"
|
|||
config :pleroma, configurable_from_database: false
|
||||
|
||||
config :pleroma, Pleroma.Repo,
|
||||
parameters: [gin_fuzzy_search_limit: "500"],
|
||||
parameters: [gin_fuzzy_search_limit: "500", jit: "off"],
|
||||
prepare: :unnamed
|
||||
|
||||
config :pleroma, :connections_pool,
|
||||
|
|
|
@ -574,6 +574,12 @@ config :pleroma, :config_description, [
|
|||
"https://status.pleroma.example.org"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :contact_username,
|
||||
type: :string,
|
||||
description: "Instance owner username",
|
||||
suggestions: ["admin"]
|
||||
},
|
||||
%{
|
||||
key: :limit,
|
||||
type: :integer,
|
||||
|
@ -3516,7 +3522,7 @@ config :pleroma, :config_description, [
|
|||
},
|
||||
%{
|
||||
key: :initial_indexing_chunk_size,
|
||||
type: :int,
|
||||
type: :integer,
|
||||
description:
|
||||
"Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
|
||||
" since there's a limit on maximum insert size",
|
||||
|
|
|
@ -49,7 +49,7 @@ config :pleroma, Pleroma.Repo,
|
|||
hostname: System.get_env("DB_HOST") || "localhost",
|
||||
port: System.get_env("DB_PORT") || "5432",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 50
|
||||
pool_size: System.schedulers_online() * 2
|
||||
|
||||
config :pleroma, :dangerzone, override_repo_pool_size: true
|
||||
|
||||
|
@ -61,7 +61,8 @@ config :tesla, adapter: Tesla.Mock
|
|||
config :pleroma, :rich_media,
|
||||
enabled: false,
|
||||
ignore_hosts: [],
|
||||
ignore_tld: ["local", "localdomain", "lan"]
|
||||
ignore_tld: ["local", "localdomain", "lan"],
|
||||
max_body: 2_000_000
|
||||
|
||||
config :pleroma, :instance,
|
||||
multi_factor_authentication: [
|
||||
|
@ -174,6 +175,8 @@ config :pleroma, Pleroma.Uploaders.Uploader, timeout: 1_000
|
|||
|
||||
config :pleroma, Pleroma.Emoji.Loader, test_emoji: true
|
||||
|
||||
config :pleroma, Pleroma.Web.RichMedia.Backfill, provider: Pleroma.Web.RichMedia.Backfill
|
||||
|
||||
if File.exists?("./config/test.secret.exs") do
|
||||
import_config "test.secret.exs"
|
||||
else
|
||||
|
|
|
@ -161,7 +161,8 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
|
||||
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
|
||||
* `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
|
||||
* `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions).
|
||||
* `Pleroma.Web.ActivityPub.MRF.ForceMention`: Forces posts to include a mention of the author of parent post or the author of quoted post.
|
||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||
|
||||
|
@ -272,6 +273,10 @@ Notes:
|
|||
#### :mrf_inline_quote
|
||||
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `<bdi>RT:</bdi> {url}`
|
||||
|
||||
#### :mrf_force_mention
|
||||
* `mention_parent`: Whether to append mention of parent post author
|
||||
* `mention_quoted`: Whether to append mention of parent quoted author
|
||||
|
||||
### :activitypub
|
||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||
|
|
|
@ -1751,3 +1751,53 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns
|
|||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
|
||||
## `GET /api/v1/pleroma/admin/rules`
|
||||
|
||||
### List rules
|
||||
|
||||
- Response: JSON, list of rules
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"priority": 1,
|
||||
"text": "There are no rules",
|
||||
"hint": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## `POST /api/v1/pleroma/admin/rules`
|
||||
|
||||
### Create a rule
|
||||
|
||||
- Params:
|
||||
- `text`: string, required, rule content
|
||||
- `hint`: string, optional, rule description
|
||||
- `priority`: integer, optional, rule ordering priority
|
||||
|
||||
- Response: JSON, a single rule
|
||||
|
||||
## `PATCH /api/v1/pleroma/admin/rules/:id`
|
||||
|
||||
### Update a rule
|
||||
|
||||
- Params:
|
||||
- `text`: string, optional, rule content
|
||||
- `hint`: string, optional, rule description
|
||||
- `priority`: integer, optional, rule ordering priority
|
||||
|
||||
- Response: JSON, a single rule
|
||||
|
||||
## `DELETE /api/v1/pleroma/admin/rules/:id`
|
||||
|
||||
### Delete a rule
|
||||
|
||||
- Response: JSON, empty object
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
|
|
@ -40,6 +40,8 @@ Has these additional fields under the `pleroma` object:
|
|||
- `parent_visible`: If the parent of this post is visible to the user or not.
|
||||
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
|
||||
- `quotes_count`: the count of status quotes.
|
||||
- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
|
||||
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
|
||||
|
||||
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
|
||||
|
||||
|
@ -65,6 +67,12 @@ Some apps operate under the assumption that no more than 4 attachments can be re
|
|||
|
||||
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
|
||||
|
||||
## Bookmarks
|
||||
|
||||
The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
|
||||
|
||||
The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
|
||||
|
||||
## Accounts
|
||||
|
||||
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
|
||||
|
|
|
@ -283,6 +283,52 @@ See [Admin-API](admin_api.md)
|
|||
* `id`: the id of the status
|
||||
* Response: JSON, returns a list of Mastodon Status entities
|
||||
|
||||
## `GET /api/v1/pleroma/bookmark_folders`
|
||||
### Gets user bookmark folders
|
||||
* Authentication: required
|
||||
|
||||
* Response: JSON. Returns a list of bookmark folders.
|
||||
* Example response:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "9umDrYheeY451cQnEe",
|
||||
"name": "Read later",
|
||||
"emoji": "🕓",
|
||||
"source": {
|
||||
"emoji": "🕓"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## `POST /api/v1/pleroma/bookmark_folders`
|
||||
### Creates a bookmark folder
|
||||
* Authentication: required
|
||||
|
||||
* Params:
|
||||
* `name`: folder name
|
||||
* `emoji`: folder emoji (optional)
|
||||
* Response: JSON. Returns a single bookmark folder.
|
||||
|
||||
## `PATCH /api/v1/pleroma/bookmark_folders/:id`
|
||||
### Updates a bookmark folder
|
||||
* Authentication: required
|
||||
|
||||
* Params:
|
||||
* `id`: folder id
|
||||
* `name`: folder name (optional)
|
||||
* `emoji`: folder emoji (optional)
|
||||
* Response: JSON. Returns a single bookmark folder.
|
||||
|
||||
## `DELETE /api/v1/pleroma/bookmark_folders/:id`
|
||||
### Deletes a bookmark folder
|
||||
* Authentication: required
|
||||
|
||||
* Params:
|
||||
* `id`: folder id
|
||||
* Response: JSON. Returns a single bookmark folder.
|
||||
|
||||
## `/api/v1/pleroma/mascot`
|
||||
### Gets user mascot image
|
||||
* Method `GET`
|
||||
|
|
|
@ -12,8 +12,8 @@ Note: This article is potentially outdated because at this time we may not have
|
|||
|
||||
### 必要なソフトウェア
|
||||
|
||||
- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
|
||||
- `postgresql-contrib` 9.6以上 (同上)
|
||||
- PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
|
||||
- `postgresql-contrib` 11.0以上 (同上)
|
||||
- Elixir 1.8 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
|
||||
- `erlang-dev`
|
||||
- `erlang-nox`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
## Required dependencies
|
||||
|
||||
* PostgreSQL >=9.6
|
||||
* PostgreSQL >=11.0
|
||||
* Elixir >=1.11.0 <1.15
|
||||
* Erlang OTP >=22.2.0 (supported: <27)
|
||||
* git
|
||||
|
|
|
@ -119,28 +119,7 @@ defmodule Pleroma.Application do
|
|||
max_restarts = Application.get_env(:pleroma, __MODULE__)[:max_restarts]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
|
||||
result = Supervisor.start_link(children, opts)
|
||||
|
||||
set_postgres_server_version()
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp set_postgres_server_version do
|
||||
version =
|
||||
with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
|
||||
{num, _} <- Float.parse(version) do
|
||||
num
|
||||
else
|
||||
e ->
|
||||
Logger.warning(
|
||||
"Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
|
||||
)
|
||||
|
||||
9.6
|
||||
end
|
||||
|
||||
:persistent_term.put({Pleroma.Repo, :postgres_version}, version)
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
def load_custom_modules do
|
||||
|
@ -177,6 +156,7 @@ defmodule Pleroma.Application do
|
|||
build_cachex("web_resp", limit: 2500),
|
||||
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
|
||||
build_cachex("failed_proxy_url", limit: 2500),
|
||||
build_cachex("failed_media_helper_url", default_ttl: :timer.minutes(15), limit: 2_500),
|
||||
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
|
||||
build_cachex("chat_message_id_idempotency_key",
|
||||
expiration: chat_message_id_idempotency_key_expiration(),
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
|> check_welcome_message_config!()
|
||||
|> check_rum!()
|
||||
|> check_repo_pool_size!()
|
||||
|> check_mrfs()
|
||||
|> handle_result()
|
||||
end
|
||||
|
||||
|
@ -234,4 +235,25 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
true
|
||||
end
|
||||
end
|
||||
|
||||
defp check_mrfs(:ok) do
|
||||
mrfs = Config.get!([:mrf, :policies])
|
||||
|
||||
missing_mrfs =
|
||||
Enum.reduce(mrfs, [], fn x, acc ->
|
||||
if Code.ensure_compiled(x) do
|
||||
acc
|
||||
else
|
||||
acc ++ [x]
|
||||
end
|
||||
end)
|
||||
|
||||
if Enum.empty?(missing_mrfs) do
|
||||
:ok
|
||||
else
|
||||
{:error, "The following MRF modules are configured but missing: #{inspect(missing_mrfs)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_mrfs(result), do: result
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Bookmark do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
|
@ -18,33 +19,46 @@ defmodule Pleroma.Bookmark do
|
|||
schema "bookmarks" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@spec create(Ecto.UUID.t(), Ecto.UUID.t()) ::
|
||||
{:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(user_id, activity_id) do
|
||||
def create(user_id, activity_id, folder_id \\ nil) do
|
||||
attrs = %{
|
||||
user_id: user_id,
|
||||
activity_id: activity_id
|
||||
activity_id: activity_id,
|
||||
folder_id: folder_id
|
||||
}
|
||||
|
||||
%Bookmark{}
|
||||
|> cast(attrs, [:user_id, :activity_id])
|
||||
|> cast(attrs, [:user_id, :activity_id, :folder_id])
|
||||
|> validate_required([:user_id, :activity_id])
|
||||
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|
||||
|> Repo.insert()
|
||||
|> Repo.insert(
|
||||
on_conflict: [set: [folder_id: folder_id]],
|
||||
conflict_target: [:user_id, :activity_id]
|
||||
)
|
||||
end
|
||||
|
||||
@spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
|
||||
def for_user_query(user_id) do
|
||||
def for_user_query(user_id, folder_id \\ nil) do
|
||||
Bookmark
|
||||
|> where(user_id: ^user_id)
|
||||
|> maybe_filter_by_folder(folder_id)
|
||||
|> join(:inner, [b], activity in assoc(b, :activity))
|
||||
|> preload([b, a], activity: a)
|
||||
end
|
||||
|
||||
defp maybe_filter_by_folder(query, nil), do: query
|
||||
|
||||
defp maybe_filter_by_folder(query, folder_id) do
|
||||
query
|
||||
|> where(folder_id: ^folder_id)
|
||||
end
|
||||
|
||||
def get(user_id, activity_id) do
|
||||
Bookmark
|
||||
|> where(user_id: ^user_id)
|
||||
|
@ -62,4 +76,11 @@ defmodule Pleroma.Bookmark do
|
|||
|> Repo.one()
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def set_folder(bookmark, folder_id) do
|
||||
bookmark
|
||||
|> cast(%{folder_id: folder_id}, [:folder_id])
|
||||
|> validate_required([:folder_id])
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
115
lib/pleroma/bookmark_folder.ex
Normal file
115
lib/pleroma/bookmark_folder.ex
Normal file
|
@ -0,0 +1,115 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.BookmarkFolder do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
|
||||
schema "bookmark_folders" do
|
||||
field(:name, :string)
|
||||
field(:emoji, :string)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id)
|
||||
|
||||
def create(user_id, name, emoji \\ nil) do
|
||||
%BookmarkFolder{}
|
||||
|> cast(
|
||||
%{
|
||||
user_id: user_id,
|
||||
name: name,
|
||||
emoji: emoji
|
||||
},
|
||||
[:user_id, :name, :emoji]
|
||||
)
|
||||
|> validate_required([:user_id, :name])
|
||||
|> fix_emoji()
|
||||
|> validate_emoji()
|
||||
|> unique_constraint([:user_id, :name])
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def update(folder_id, name, emoji \\ nil) do
|
||||
get_by_id(folder_id)
|
||||
|> cast(
|
||||
%{
|
||||
name: name,
|
||||
emoji: emoji
|
||||
},
|
||||
[:name, :emoji]
|
||||
)
|
||||
|> fix_emoji()
|
||||
|> validate_emoji()
|
||||
|> unique_constraint([:user_id, :name])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
defp fix_emoji(changeset) do
|
||||
with {:emoji_field, emoji} when is_binary(emoji) <-
|
||||
{:emoji_field, get_field(changeset, :emoji)},
|
||||
{:fixed_emoji, emoji} <-
|
||||
{:fixed_emoji,
|
||||
emoji
|
||||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()} do
|
||||
put_change(changeset, :emoji, emoji)
|
||||
else
|
||||
{:emoji_field, _} -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_emoji(changeset) do
|
||||
validate_change(changeset, :emoji, fn
|
||||
:emoji, nil ->
|
||||
[]
|
||||
|
||||
:emoji, emoji ->
|
||||
if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do
|
||||
[]
|
||||
else
|
||||
[emoji: "Invalid emoji"]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp valid_local_custom_emoji?(emoji) do
|
||||
with %{file: _path} <- Emoji.get(emoji) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def delete(folder_id) do
|
||||
BookmarkFolder
|
||||
|> Repo.get_by(id: folder_id)
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def for_user(user_id) do
|
||||
BookmarkFolder
|
||||
|> where(user_id: ^user_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def belongs_to_user?(folder_id, user_id) do
|
||||
BookmarkFolder
|
||||
|> where(id: ^folder_id, user_id: ^user_id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
end
|
|
@ -20,6 +20,7 @@ defmodule Pleroma.Constants do
|
|||
"deleted_activity_id",
|
||||
"pleroma_internal",
|
||||
"generator",
|
||||
"rules",
|
||||
"language"
|
||||
]
|
||||
)
|
||||
|
|
|
@ -241,13 +241,13 @@ defmodule Pleroma.FollowingRelationship do
|
|||
end
|
||||
|
||||
@doc """
|
||||
For a query with joined activity,
|
||||
keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
|
||||
For a query with joined activity's actor,
|
||||
keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
|
||||
"""
|
||||
def keep_following_or_not_domain_blocked(query, user) do
|
||||
where(
|
||||
query,
|
||||
[_, activity],
|
||||
[_, user_actor: user_actor],
|
||||
fragment(
|
||||
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
|
||||
"""
|
||||
|
@ -255,9 +255,9 @@ defmodule Pleroma.FollowingRelationship do
|
|||
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
|
||||
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
|
||||
""",
|
||||
activity.actor,
|
||||
user_actor.ap_id,
|
||||
^user.domain_blocks,
|
||||
activity.actor,
|
||||
user_actor.ap_id,
|
||||
^User.binary_id(user.id),
|
||||
^accept_state_code()
|
||||
)
|
||||
|
|
|
@ -12,6 +12,8 @@ defmodule Pleroma.Helpers.MediaHelper do
|
|||
|
||||
require Logger
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
def missing_dependencies do
|
||||
Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
|
||||
if Pleroma.Utils.command_available?(executable) do
|
||||
|
@ -40,12 +42,16 @@ defmodule Pleroma.Helpers.MediaHelper do
|
|||
end
|
||||
|
||||
# Note: video thumbnail is intentionally not resized (always has original dimensions)
|
||||
@spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
|
||||
def video_framegrab(url) do
|
||||
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
|
||||
false <- @cachex.exists?(:failed_media_helper_cache, url),
|
||||
{:ok, env} <- HTTP.get(url, [], pool: :media),
|
||||
{:ok, pid} <- StringIO.open(env.body) do
|
||||
body_stream = IO.binstream(pid, 1)
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
Exile.stream!(
|
||||
[
|
||||
executable,
|
||||
|
@ -62,6 +68,17 @@ defmodule Pleroma.Helpers.MediaHelper do
|
|||
stderr: :disable
|
||||
)
|
||||
|> Enum.into(<<>>)
|
||||
end)
|
||||
|
||||
case Task.yield(task, 5_000) do
|
||||
nil ->
|
||||
Task.shutdown(task)
|
||||
@cachex.put(:failed_media_helper_cache, url, nil)
|
||||
{:error, {:ffmpeg, :timeout}}
|
||||
|
||||
result ->
|
||||
{:ok, result}
|
||||
end
|
||||
else
|
||||
nil -> {:error, {:ffmpeg, :command_not_found}}
|
||||
{:error, _} = error -> error
|
||||
|
|
|
@ -65,20 +65,16 @@ defmodule Pleroma.HTML do
|
|||
end
|
||||
end
|
||||
|
||||
@spec extract_first_external_url_from_object(Pleroma.Object.t()) ::
|
||||
{:ok, String.t()} | {:error, :no_content}
|
||||
@spec extract_first_external_url_from_object(Pleroma.Object.t()) :: String.t() | nil
|
||||
def extract_first_external_url_from_object(%{data: %{"content" => content}})
|
||||
when is_binary(content) do
|
||||
url =
|
||||
content
|
||||
|> Floki.parse_fragment!()
|
||||
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
|
||||
|> Enum.take(1)
|
||||
|> Floki.attribute("href")
|
||||
|> Enum.at(0)
|
||||
|
||||
{:ok, url}
|
||||
end
|
||||
|
||||
def extract_first_external_url_from_object(_), do: {:error, :no_content}
|
||||
def extract_first_external_url_from_object(_), do: nil
|
||||
end
|
||||
|
|
|
@ -137,7 +137,7 @@ defmodule Pleroma.Notification do
|
|||
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^blocked_ap_ids)
|
||||
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|
||||
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
|
||||
end
|
||||
|
||||
|
@ -148,7 +148,7 @@ defmodule Pleroma.Notification do
|
|||
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^blocker_ap_ids)
|
||||
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -161,7 +161,7 @@ defmodule Pleroma.Notification do
|
|||
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^notification_muted_ap_ids)
|
||||
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|
||||
|> join(:left, [n, a], tm in ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
|
||||
as: :thread_mute
|
||||
|
@ -361,36 +361,32 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
|
||||
def create_notifications(activity, options \\ [])
|
||||
@spec create_notifications(Activity.t()) :: {:ok, [Notification.t()] | []}
|
||||
def create_notifications(activity)
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
if object && object.data["type"] == "Answer" do
|
||||
{:ok, []}
|
||||
else
|
||||
do_create_notifications(activity, options)
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
|
||||
do_create_notifications(activity, options)
|
||||
do_create_notifications(activity)
|
||||
end
|
||||
|
||||
def create_notifications(_, _), do: {:ok, []}
|
||||
def create_notifications(_), do: {:ok, []}
|
||||
|
||||
defp do_create_notifications(%Activity{} = activity, options) do
|
||||
do_send = Keyword.get(options, :do_send, true)
|
||||
|
||||
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
|
||||
potential_receivers = enabled_receivers ++ disabled_receivers
|
||||
defp do_create_notifications(%Activity{} = activity) do
|
||||
enabled_receivers = get_notified_from_activity(activity)
|
||||
|
||||
notifications =
|
||||
Enum.map(potential_receivers, fn user ->
|
||||
do_send = do_send && user in enabled_receivers
|
||||
create_notification(activity, user, do_send: do_send)
|
||||
Enum.map(enabled_receivers, fn user ->
|
||||
create_notification(activity, user)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
|
@ -450,7 +446,6 @@ defmodule Pleroma.Notification do
|
|||
|
||||
# TODO move to sql, too.
|
||||
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
|
||||
do_send = Keyword.get(opts, :do_send, true)
|
||||
type = Keyword.get(opts, :type, type_from_activity(activity))
|
||||
|
||||
unless skip?(activity, user, opts) do
|
||||
|
@ -465,11 +460,6 @@ defmodule Pleroma.Notification do
|
|||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
|
||||
if do_send do
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end
|
||||
|
||||
notification
|
||||
end
|
||||
end
|
||||
|
@ -527,10 +517,7 @@ defmodule Pleroma.Notification do
|
|||
|> exclude_relationship_restricted_ap_ids(activity)
|
||||
|> exclude_thread_muter_ap_ids(activity)
|
||||
|
||||
notification_enabled_users =
|
||||
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
|
||||
|
||||
{notification_enabled_users, potential_receivers -- notification_enabled_users}
|
||||
end
|
||||
|
||||
def get_notified_from_activity(_, _local_only), do: {[], []}
|
||||
|
@ -643,6 +630,7 @@ defmodule Pleroma.Notification do
|
|||
def skip?(%Activity{} = activity, %User{} = user, opts) do
|
||||
[
|
||||
:self,
|
||||
:internal,
|
||||
:invisible,
|
||||
:block_from_strangers,
|
||||
:recently_followed,
|
||||
|
@ -662,6 +650,12 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
end
|
||||
|
||||
def skip?(:internal, %Activity{} = activity, _user, _opts) do
|
||||
actor = activity.data["actor"]
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
User.internal?(user)
|
||||
end
|
||||
|
||||
def skip?(:invisible, %Activity{} = activity, _user, _opts) do
|
||||
actor = activity.data["actor"]
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
@ -748,4 +742,12 @@ defmodule Pleroma.Notification do
|
|||
)
|
||||
|> Repo.update_all(set: [seen: true])
|
||||
end
|
||||
|
||||
@spec send(list(Notification.t())) :: :ok
|
||||
def send(notifications) do
|
||||
Enum.each(notifications, fn notification ->
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
68
lib/pleroma/rule.ex
Normal file
68
lib/pleroma/rule.ex
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Rule do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Rule
|
||||
|
||||
schema "rules" do
|
||||
field(:priority, :integer, default: 0)
|
||||
field(:text, :string)
|
||||
field(:hint, :string)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(%Rule{} = rule, params \\ %{}) do
|
||||
rule
|
||||
|> cast(params, [:priority, :text, :hint])
|
||||
|> validate_required([:text])
|
||||
end
|
||||
|
||||
def query do
|
||||
Rule
|
||||
|> order_by(asc: :priority)
|
||||
|> order_by(asc: :id)
|
||||
end
|
||||
|
||||
def get(ids) when is_list(ids) do
|
||||
from(r in __MODULE__, where: r.id in ^ids)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get(id), do: Repo.get(__MODULE__, id)
|
||||
|
||||
def exists?(id) do
|
||||
from(r in __MODULE__, where: r.id == ^id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def create(params) do
|
||||
{:ok, rule} =
|
||||
%Rule{}
|
||||
|> changeset(params)
|
||||
|> Repo.insert()
|
||||
|
||||
rule
|
||||
end
|
||||
|
||||
def update(params, id) do
|
||||
{:ok, rule} =
|
||||
get(id)
|
||||
|> changeset(params)
|
||||
|> Repo.update()
|
||||
|
||||
rule
|
||||
end
|
||||
|
||||
def delete(id) do
|
||||
get(id)
|
||||
|> Repo.delete()
|
||||
end
|
||||
end
|
|
@ -23,19 +23,12 @@ defmodule Pleroma.Search.DatabaseSearch do
|
|||
offset = Keyword.get(options, :offset, 0)
|
||||
author = Keyword.get(options, :author)
|
||||
|
||||
search_function =
|
||||
if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do
|
||||
:websearch
|
||||
else
|
||||
:plain
|
||||
end
|
||||
|
||||
try do
|
||||
Activity
|
||||
|> Activity.with_preloaded_object()
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> restrict_public(user)
|
||||
|> query_with(index_type, search_query, search_function)
|
||||
|> query_with(index_type, search_query, :websearch)
|
||||
|> maybe_restrict_local(user)
|
||||
|> maybe_restrict_author(author)
|
||||
|> maybe_restrict_blocked(user)
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.User do
|
|||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Ecto, only: [assoc: 2]
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Ecto.Multi
|
||||
alias Pleroma.Activity
|
||||
|
@ -596,9 +597,23 @@ defmodule Pleroma.User do
|
|||
|
||||
defp put_fields(changeset) do
|
||||
if raw_fields = get_change(changeset, :raw_fields) do
|
||||
old_fields = changeset.data.raw_fields
|
||||
|
||||
raw_fields =
|
||||
raw_fields
|
||||
|> Enum.filter(fn %{"name" => n} -> n != "" end)
|
||||
|> Enum.map(fn field ->
|
||||
previous =
|
||||
old_fields
|
||||
|> Enum.find(fn %{"value" => value} -> field["value"] == value end)
|
||||
|
||||
if previous && Map.has_key?(previous, "verified_at") do
|
||||
field
|
||||
|> Map.put("verified_at", previous["verified_at"])
|
||||
else
|
||||
field
|
||||
end
|
||||
end)
|
||||
|
||||
fields =
|
||||
raw_fields
|
||||
|
@ -1200,6 +1215,10 @@ defmodule Pleroma.User do
|
|||
|
||||
def update_and_set_cache(changeset) do
|
||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||
if get_change(changeset, :raw_fields) do
|
||||
BackgroundWorker.enqueue("verify_fields_links", %{"user_id" => user.id})
|
||||
end
|
||||
|
||||
set_cache(user)
|
||||
end
|
||||
end
|
||||
|
@ -1975,8 +1994,45 @@ defmodule Pleroma.User do
|
|||
maybe_delete_from_db(user)
|
||||
end
|
||||
|
||||
def perform(:verify_fields_links, user) do
|
||||
profile_urls = [user.ap_id]
|
||||
|
||||
fields =
|
||||
user.raw_fields
|
||||
|> Enum.map(&verify_field_link(&1, profile_urls))
|
||||
|
||||
changeset =
|
||||
user
|
||||
|> update_changeset(%{raw_fields: fields})
|
||||
|
||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||
set_cache(user)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(:set_activation_async, user, status), do: set_activation(user, status)
|
||||
|
||||
defp verify_field_link(field, profile_urls) do
|
||||
verified_at =
|
||||
with %{"value" => value} <- field,
|
||||
{:verified_at, nil} <- {:verified_at, Map.get(field, "verified_at")},
|
||||
%{scheme: scheme, userinfo: nil, host: host}
|
||||
when not_empty_string(host) and scheme in ["http", "https"] <-
|
||||
URI.parse(value),
|
||||
{:not_idn, true} <- {:not_idn, to_string(:idna.encode(host)) == host},
|
||||
"me" <- Pleroma.Web.RelMe.maybe_put_rel_me(value, profile_urls) do
|
||||
CommonUtils.to_masto_date(NaiveDateTime.utc_now())
|
||||
else
|
||||
{:verified_at, value} when not_empty_string(value) ->
|
||||
value
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
Map.put(field, "verified_at", verified_at)
|
||||
end
|
||||
|
||||
@spec external_users_query() :: Ecto.Query.t()
|
||||
def external_users_query do
|
||||
User.Query.build(%{
|
||||
|
@ -2664,10 +2720,11 @@ defmodule Pleroma.User do
|
|||
# - display name
|
||||
def sanitize_html(%User{} = user, filter) do
|
||||
fields =
|
||||
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
|
||||
Enum.map(user.fields, fn %{"name" => name, "value" => value} = fields ->
|
||||
%{
|
||||
"name" => name,
|
||||
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
|
||||
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly),
|
||||
"verified_at" => Map.get(fields, "verified_at")
|
||||
}
|
||||
end)
|
||||
|
||||
|
|
|
@ -196,7 +196,14 @@ defmodule Pleroma.User.Backup do
|
|||
end
|
||||
end
|
||||
|
||||
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
|
||||
@files [
|
||||
'actor.json',
|
||||
'outbox.json',
|
||||
'likes.json',
|
||||
'bookmarks.json',
|
||||
'followers.json',
|
||||
'following.json'
|
||||
]
|
||||
@spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error
|
||||
def export(%__MODULE__{} = backup, caller_pid) do
|
||||
backup = Repo.preload(backup, :user)
|
||||
|
@ -207,6 +214,8 @@ defmodule Pleroma.User.Backup do
|
|||
:ok <- statuses(dir, backup.user, caller_pid),
|
||||
:ok <- likes(dir, backup.user, caller_pid),
|
||||
:ok <- bookmarks(dir, backup.user, caller_pid),
|
||||
:ok <- followers(dir, backup.user, caller_pid),
|
||||
:ok <- following(dir, backup.user, caller_pid),
|
||||
{:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir),
|
||||
{:ok, _} <- File.rm_rf(dir) do
|
||||
{:ok, zip_path}
|
||||
|
@ -357,6 +366,16 @@ defmodule Pleroma.User.Backup do
|
|||
caller_pid
|
||||
)
|
||||
end
|
||||
|
||||
defp followers(dir, user, caller_pid) do
|
||||
User.get_followers_query(user)
|
||||
|> write(dir, "followers", fn a -> {:ok, a.ap_id} end, caller_pid)
|
||||
end
|
||||
|
||||
defp following(dir, user, caller_pid) do
|
||||
User.get_friends_query(user)
|
||||
|> write(dir, "following", fn a -> {:ok, a.ap_id} end, caller_pid)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Pleroma.User.Backup.ProcessorAPI do
|
||||
|
|
|
@ -147,9 +147,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
# Splice in the child object if we have one.
|
||||
activity = Maps.put_if_present(activity, :object, object)
|
||||
|
||||
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
|
||||
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
|
||||
end)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
|
||||
# Add local posts to search index
|
||||
if local, do: Pleroma.Search.add_to_index(activity)
|
||||
|
@ -177,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
id: "pleroma:fakeid"
|
||||
}
|
||||
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
{:ok, activity}
|
||||
|
||||
{:remote_limit_pass, _} ->
|
||||
|
@ -202,7 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
def notify_and_stream(activity) do
|
||||
Notification.create_notifications(activity)
|
||||
{:ok, notifications} = Notification.create_notifications(activity)
|
||||
Notification.send(notifications)
|
||||
|
||||
original_activity =
|
||||
case activity do
|
||||
|
@ -1261,6 +1260,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_quote_url(query, _), do: query
|
||||
|
||||
defp restrict_rule(query, %{rule_id: rule_id}) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_rule(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
@ -1423,6 +1431,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_instance(opts)
|
||||
|> restrict_announce_object_actor(opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> restrict_rule(opts)
|
||||
|> restrict_quote_url(opts)
|
||||
|> maybe_restrict_deactivated_users(opts)
|
||||
|> exclude_poll_votes(opts)
|
||||
|
|
59
lib/pleroma/web/activity_pub/mrf/force_mention.ex
Normal file
59
lib/pleroma/web/activity_pub/mrf/force_mention.ex
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp get_author(url) do
|
||||
with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
|
||||
%User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
|
||||
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp prepend_author(tags, _, false), do: tags
|
||||
|
||||
defp prepend_author(tags, nil, _), do: tags
|
||||
|
||||
defp prepend_author(tags, url, _) do
|
||||
actor = get_author(url)
|
||||
|
||||
if not is_nil(actor) do
|
||||
[actor | tags]
|
||||
else
|
||||
tags
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
|
||||
tag =
|
||||
tag
|
||||
|> prepend_author(
|
||||
object["inReplyTo"],
|
||||
Config.get([:mrf_force_mention, :mention_parent, true])
|
||||
)
|
||||
|> prepend_author(
|
||||
object["quoteUrl"],
|
||||
Config.get([:mrf_force_mention, :mention_quoted, true])
|
||||
)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, put_in(activity["object"]["tag"], tag)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
end
|
|
@ -12,13 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
|||
@primary_key false
|
||||
embedded_schema do
|
||||
field(:id, :string)
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Link")
|
||||
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
|
||||
field(:name, :string)
|
||||
field(:blurhash, :string)
|
||||
|
||||
embeds_many :url, UrlObjectValidator, primary_key: false do
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Link")
|
||||
field(:href, ObjectValidators.Uri)
|
||||
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
|
||||
field(:width, :integer)
|
||||
|
|
|
@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
|
|||
|
||||
embeds_one :replies, Replies, primary_key: false do
|
||||
field(:totalItems, :integer)
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Collection")
|
||||
end
|
||||
|
||||
field(:type, :string)
|
||||
field(:type, :string, default: "Note")
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
|
|
|
@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
|
||||
field(:closed, ObjectValidators.DateTime)
|
||||
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:nonAnonymous, :boolean)
|
||||
embeds_many(:anyOf, QuestionOptionsValidator)
|
||||
embeds_many(:oneOf, QuestionOptionsValidator)
|
||||
end
|
||||
|
|
|
@ -158,10 +158,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
end
|
||||
end
|
||||
|
||||
defp should_federate?(inbox, public) do
|
||||
if public do
|
||||
true
|
||||
else
|
||||
def should_federate?(nil, _), do: false
|
||||
def should_federate?(_, true), do: true
|
||||
|
||||
def should_federate?(inbox, _) do
|
||||
%{host: host} = URI.parse(inbox)
|
||||
|
||||
quarantined_instances =
|
||||
|
@ -171,7 +171,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|
||||
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
|
||||
end
|
||||
end
|
||||
|
||||
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
|
||||
defp recipients(actor, activity) do
|
||||
|
|
|
@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Workers.PollWorker
|
||||
|
||||
|
@ -125,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
nil
|
||||
end
|
||||
|
||||
{:ok, notifications} = Notification.create_notifications(object, do_send: false)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|
@ -184,7 +183,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
liked_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_like_to_object(object, liked_object)
|
||||
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
@ -202,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
||||
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
|
||||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||
{:ok, notifications} = Notification.create_notifications(activity)
|
||||
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
|
||||
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
|
||||
|
||||
|
@ -227,9 +230,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
end
|
||||
|
||||
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
|
||||
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
|
||||
end)
|
||||
Pleroma.Web.RichMedia.Card.get_by_activity(activity)
|
||||
|
||||
Pleroma.Search.add_to_index(Map.put(activity, :object, object))
|
||||
|
||||
|
@ -258,11 +259,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
Utils.add_announce_to_object(object, announced_object)
|
||||
|
||||
if !User.internal?(user) do
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
ap_streamer().stream_out(object)
|
||||
end
|
||||
if !User.internal?(user), do: ap_streamer().stream_out(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
@ -283,7 +286,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
reacted_object = Object.get_by_ap_id(object.data["object"])
|
||||
Utils.add_emoji_reaction_to_object(object, reacted_object)
|
||||
|
||||
Notification.create_notifications(object)
|
||||
{:ok, notifications} = Notification.create_notifications(object)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
@ -587,10 +594,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end)
|
||||
|> Notification.send()
|
||||
|
||||
meta
|
||||
end
|
||||
|
|
|
@ -728,14 +728,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
#### Flag-related helpers
|
||||
@spec make_flag_data(map(), map()) :: map()
|
||||
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
|
||||
def make_flag_data(
|
||||
%{actor: actor, context: context, content: content} = params,
|
||||
additional
|
||||
) do
|
||||
%{
|
||||
"type" => "Flag",
|
||||
"actor" => actor.ap_id,
|
||||
"content" => content,
|
||||
"object" => build_flag_object(params),
|
||||
"context" => context,
|
||||
"state" => "open"
|
||||
"state" => "open",
|
||||
"rules" => Map.get(params, :rules, nil)
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
|
|
@ -67,8 +67,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
def render("user.json", %{user: %User{nickname: nil} = user}),
|
||||
do: render("service.json", %{user: user})
|
||||
|
||||
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
|
||||
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
||||
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do
|
||||
render("service.json", %{user: user})
|
||||
|> Map.merge(%{
|
||||
"preferredUsername" => user.nickname,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
})
|
||||
end
|
||||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
|
@ -121,7 +126,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"discoverable" => user.is_discoverable,
|
||||
"capabilities" => capabilities,
|
||||
"alsoKnownAs" => user.also_known_as,
|
||||
"vcard:bday" => birthday
|
||||
"vcard:bday" => birthday,
|
||||
"webfinger" => "acct:#{User.full_nickname(user)}"
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|
|
62
lib/pleroma/web/admin_api/controllers/rule_controller.ex
Normal file
62
lib/pleroma/web/admin_api/controllers/rule_controller.ex
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.RuleController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
json_response: 3
|
||||
]
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write"]}
|
||||
when action in [:create, :update, :delete]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
|
||||
|
||||
action_fallback(AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
|
||||
|
||||
def index(conn, _) do
|
||||
rules =
|
||||
Rule.query()
|
||||
|> Repo.all()
|
||||
|
||||
render(conn, "index.json", rules: rules)
|
||||
end
|
||||
|
||||
def create(%{body_params: params} = conn, _) do
|
||||
rule =
|
||||
params
|
||||
|> Rule.create()
|
||||
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
|
||||
def update(%{body_params: params} = conn, %{id: id}) do
|
||||
rule =
|
||||
params
|
||||
|> Rule.update(id)
|
||||
|
||||
render(conn, "show.json", rule: rule)
|
||||
end
|
||||
|
||||
def delete(conn, %{id: id}) do
|
||||
with {:ok, _} <- Rule.delete(id) do
|
||||
json(conn, %{})
|
||||
else
|
||||
_ -> json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,9 +6,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
alias Pleroma.Web.AdminAPI.RuleView
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
|
@ -46,7 +48,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
as: :activity
|
||||
}),
|
||||
state: report.data["state"],
|
||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
|
||||
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
|
||||
rules: rules(Map.get(report.data, "rules", nil))
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -71,4 +74,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
|
|||
created_at: Utils.to_masto_date(inserted_at)
|
||||
}
|
||||
end
|
||||
|
||||
defp rules(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp rules(rule_ids) do
|
||||
rules =
|
||||
rule_ids
|
||||
|> Rule.get()
|
||||
|
||||
render(RuleView, "index.json", rules: rules)
|
||||
end
|
||||
end
|
||||
|
|
22
lib/pleroma/web/admin_api/views/rule_view.ex
Normal file
22
lib/pleroma/web/admin_api/views/rule_view.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.RuleView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def render("index.json", %{rules: rules} = _opts) do
|
||||
render_many(rules, __MODULE__, "show.json")
|
||||
end
|
||||
|
||||
def render("show.json", %{rule: rule} = _opts) do
|
||||
%{
|
||||
id: to_string(rule.id),
|
||||
priority: rule.priority,
|
||||
text: rule.text,
|
||||
hint: rule.hint
|
||||
}
|
||||
end
|
||||
end
|
|
@ -97,6 +97,7 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Frontend management",
|
||||
"Instance configuration",
|
||||
"Instance documents",
|
||||
"Instance rule managment",
|
||||
"Invites",
|
||||
"MediaProxy cache",
|
||||
"OAuth application management",
|
||||
|
@ -137,7 +138,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Scheduled statuses",
|
||||
"Search",
|
||||
"Status actions",
|
||||
"Media attachments"
|
||||
"Media attachments",
|
||||
"Bookmark folders"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
|
|
@ -30,6 +30,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
report_state(),
|
||||
"Filter by report state"
|
||||
),
|
||||
Operation.parameter(
|
||||
:rule_id,
|
||||
:query,
|
||||
%Schema{type: :string},
|
||||
"Filter by selected rule id"
|
||||
),
|
||||
Operation.parameter(
|
||||
:limit,
|
||||
:query,
|
||||
|
@ -169,6 +175,17 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
|
|||
inserted_at: %Schema{type: :string, format: :"date-time"}
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
115
lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
Normal file
115
lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
Normal file
|
@ -0,0 +1,115 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Retrieve list of instance rules",
|
||||
operationId: "AdminAPI.RuleController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Response", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: rule()
|
||||
}),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Create new rule",
|
||||
operationId: "AdminAPI.RuleController.create",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", rule()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Modify existing rule",
|
||||
operationId: "AdminAPI.RuleController.update",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", rule()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Instance rule managment"],
|
||||
summary: "Delete rule",
|
||||
operationId: "AdminAPI.RuleController.delete",
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
responses: %{
|
||||
200 => empty_object_response(),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:text],
|
||||
properties: %{
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp rule do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
priority: %Schema{type: :integer},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -46,10 +46,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def rules_operation do
|
||||
%Operation{
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve list of instance rules",
|
||||
operationId: "InstanceController.rules",
|
||||
responses: %{
|
||||
200 => Operation.response("Array of domains", "application/json", array_of_rules())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp instance do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
max_featured_tags: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of featured tags allowed for each account."
|
||||
}
|
||||
}
|
||||
},
|
||||
uri: %Schema{type: :string, description: "The domain name of the instance"},
|
||||
title: %Schema{type: :string, description: "The title of the website"},
|
||||
description: %Schema{
|
||||
|
@ -172,7 +192,8 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
"urls" => %{
|
||||
"streaming_api" => "wss://lain.com"
|
||||
},
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)",
|
||||
"rules" => array_of_rules()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -272,6 +293,19 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
type: :object,
|
||||
description: "Instance configuration",
|
||||
properties: %{
|
||||
accounts: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
max_featured_tags: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of featured tags allowed for each account."
|
||||
},
|
||||
max_pinned_statuses: %Schema{
|
||||
type: :integer,
|
||||
description: "The maximum number of pinned statuses for each account."
|
||||
}
|
||||
}
|
||||
},
|
||||
urls: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
|
@ -285,6 +319,11 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
type: :object,
|
||||
description: "A map with poll limits for local statuses",
|
||||
properties: %{
|
||||
characters_reserved_per_url: %Schema{
|
||||
type: :integer,
|
||||
description:
|
||||
"Each URL in a status will be assumed to be exactly this many characters."
|
||||
},
|
||||
max_characters: %Schema{
|
||||
type: :integer,
|
||||
description: "Posts character limit (CW/Subject included in the counter)"
|
||||
|
@ -344,4 +383,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
example: ["pleroma.site", "lain.com", "bikeshed.party"]
|
||||
}
|
||||
end
|
||||
|
||||
defp array_of_rules do
|
||||
%Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
text: %Schema{type: :string},
|
||||
hint: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BookmarkFolder
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(any()) :: any()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "All bookmark folders",
|
||||
security: [%{"oAuth" => ["read:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.index",
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Bookmark Folders", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: BookmarkFolder
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Create a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.create",
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Update a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.update",
|
||||
parameters: [id_param()],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
422 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Bookmark folders"],
|
||||
summary: "Delete a bookmark folder",
|
||||
security: [%{"oAuth" => ["write:bookmarks"]}],
|
||||
operationId: "PleromaAPI.BookmarkFolderController.delete",
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
title: "BookmarkFolderCreateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
description: "Folder name"
|
||||
},
|
||||
emoji: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder emoji"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
title: "BookmarkFolderUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder name"
|
||||
},
|
||||
emoji: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
description: "Folder emoji"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def id_param do
|
||||
Operation.parameter(:id, :path, FlakeID.schema(), "Bookmark Folder ID",
|
||||
example: "9umDrYheeY451cQnEe",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
end
|
|
@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
default: false,
|
||||
description:
|
||||
"If the account is remote, should the report be forwarded to the remote admin?"
|
||||
},
|
||||
rule_ids: %Schema{
|
||||
type: :array,
|
||||
nullable: true,
|
||||
items: %Schema{type: :string},
|
||||
description: "Array of rules"
|
||||
}
|
||||
},
|
||||
required: [:account_id],
|
||||
|
@ -60,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
"account_id" => "123",
|
||||
"status_ids" => ["1337"],
|
||||
"comment" => "bad status!",
|
||||
"forward" => "false"
|
||||
"forward" => "false",
|
||||
"rule_ids" => ["3"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
@ -256,6 +256,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
description: "Privately bookmark a status",
|
||||
operationId: "StatusController.bookmark",
|
||||
parameters: [id_param()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
title: "StatusUpdateRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
folder_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of bookmarks folder, if any"
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses: %{
|
||||
200 => status_response()
|
||||
}
|
||||
|
@ -430,7 +442,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
summary: "Bookmarked statuses",
|
||||
description: "Statuses the user has bookmarked",
|
||||
operationId: "StatusController.bookmarks",
|
||||
parameters: pagination_params(),
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:folder_id,
|
||||
:query,
|
||||
FlakeID.schema(),
|
||||
"If provided, only display bookmarks from given folder"
|
||||
)
|
||||
| pagination_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read:bookmarks"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
|
||||
|
|
26
lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
Normal file
26
lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "BookmarkFolder",
|
||||
description: "Response schema for a bookmark folder",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
name: %Schema{type: :string, description: "Folder name"},
|
||||
emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"id" => "9toJCu5YZW7O7gfvH6",
|
||||
"name" => "Read later",
|
||||
"emoji" => nil
|
||||
}
|
||||
})
|
||||
end
|
|
@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
|||
}
|
||||
},
|
||||
description: "Possible answers for the poll."
|
||||
},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
non_anonymous: %Schema{
|
||||
type: :boolean,
|
||||
description: "Can voters be publicly identified?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
|||
votes_count: 4
|
||||
}
|
||||
],
|
||||
emojis: []
|
||||
emojis: [],
|
||||
pleroma: %{
|
||||
non_anonymous: false
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
format: :uri,
|
||||
description: "Preview thumbnail"
|
||||
},
|
||||
image_description: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate text that describes what is in the thumbnail"
|
||||
},
|
||||
title: %Schema{type: :string, description: "Title of linked resource"},
|
||||
description: %Schema{type: :string, description: "Description of preview"}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.Formatter
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Rule
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
|
@ -568,14 +569,16 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
def report(user, data) do
|
||||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
{:ok, statuses} <- get_report_statuses(account, data) do
|
||||
{:ok, statuses} <- get_report_statuses(account, data),
|
||||
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
|
||||
ActivityPub.flag(%{
|
||||
context: Utils.generate_context_id(),
|
||||
actor: user,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content_html,
|
||||
forward: Map.get(data, :forward, false)
|
||||
forward: Map.get(data, :forward, false),
|
||||
rules: rules
|
||||
})
|
||||
end
|
||||
end
|
||||
|
@ -587,6 +590,15 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_report_rules(nil) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp get_report_rules(rule_ids) do
|
||||
rule_ids
|
||||
|> Enum.filter(&Rule.exists?/1)
|
||||
end
|
||||
|
||||
def update_report_state(activity_ids, state) when is_list(activity_ids) do
|
||||
case Utils.update_report_state(activity_ids, state) do
|
||||
:ok -> {:ok, activity_ids}
|
||||
|
|
|
@ -25,4 +25,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
|
|||
def peers(conn, _params) do
|
||||
json(conn, Pleroma.Stats.get_peers())
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/instance/rules"
|
||||
def rules(conn, _params) do
|
||||
render(conn, "rules.json")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ScheduledActivity
|
||||
|
@ -37,7 +38,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
when action in [
|
||||
:index,
|
||||
:show,
|
||||
:card,
|
||||
:context,
|
||||
:show_history,
|
||||
:show_source
|
||||
|
@ -411,13 +411,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
|
||||
@doc "POST /api/v1/statuses/:id/bookmark"
|
||||
def bookmark(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||
%User{} = user <- User.get_cached_by_nickname(user.nickname),
|
||||
true <- Visibility.visible_for_user?(activity, user),
|
||||
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
|
||||
folder_id <- Map.get(body_params, :folder_id, nil),
|
||||
folder_id <-
|
||||
if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id),
|
||||
do: folder_id,
|
||||
else: nil
|
||||
),
|
||||
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
|
||||
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
|
||||
end
|
||||
end
|
||||
|
@ -463,21 +472,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/statuses/:id/card"
|
||||
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
|
||||
def card(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: status_id}}}} = conn,
|
||||
_
|
||||
) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(status_id),
|
||||
true <- Visibility.visible_for_user?(activity, user) do
|
||||
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
render(conn, "card.json", data)
|
||||
else
|
||||
_ -> render_error(conn, :not_found, "Record not found")
|
||||
end
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/statuses/:id/favourited_by"
|
||||
def favourited_by(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
|
||||
|
@ -573,10 +567,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
@doc "GET /api/v1/bookmarks"
|
||||
def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
|
||||
user = User.get_cached_by_id(user.id)
|
||||
folder_id = Map.get(params, :folder_id)
|
||||
|
||||
bookmarks =
|
||||
user.id
|
||||
|> Bookmark.for_user_query()
|
||||
|> Bookmark.for_user_query(folder_id)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
|
||||
activities =
|
||||
|
|
|
@ -28,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
|> to_string,
|
||||
registrations: Keyword.get(instance, :registrations_open),
|
||||
approval_required: Keyword.get(instance, :account_approval_required),
|
||||
contact_account: contact_account(Keyword.get(instance, :contact_username)),
|
||||
configuration: configuration(),
|
||||
# Extra (not present in Mastodon):
|
||||
max_toot_chars: Keyword.get(instance, :limit),
|
||||
|
@ -68,19 +69,33 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
},
|
||||
contact: %{
|
||||
email: Keyword.get(instance, :email),
|
||||
account: nil
|
||||
account: contact_account(Keyword.get(instance, :contact_username))
|
||||
},
|
||||
# Extra (not present in Mastodon):
|
||||
pleroma: pleroma_configuration2(instance)
|
||||
})
|
||||
end
|
||||
|
||||
def render("rules.json", _) do
|
||||
Pleroma.Rule.query()
|
||||
|> Pleroma.Repo.all()
|
||||
|> render_many(__MODULE__, "rule.json", as: :rule)
|
||||
end
|
||||
|
||||
def render("rule.json", %{rule: rule}) do
|
||||
%{
|
||||
id: to_string(rule.id),
|
||||
text: rule.text,
|
||||
hint: rule.hint || ""
|
||||
}
|
||||
end
|
||||
|
||||
defp common_information(instance) do
|
||||
%{
|
||||
title: Keyword.get(instance, :name),
|
||||
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
|
||||
languages: Keyword.get(instance, :languages, ["en"]),
|
||||
rules: []
|
||||
rules: render(__MODULE__, "rules.json"),
|
||||
title: Keyword.get(instance, :name),
|
||||
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})"
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -129,7 +144,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
"profile_directory"
|
||||
end,
|
||||
"pleroma:get:main/ostatus",
|
||||
"pleroma:group_actors"
|
||||
"pleroma:group_actors",
|
||||
"pleroma:bookmark_folders"
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
@ -170,6 +186,22 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
}
|
||||
end
|
||||
|
||||
defp contact_account(nil), do: nil
|
||||
|
||||
defp contact_account("@" <> username) do
|
||||
contact_account(username)
|
||||
end
|
||||
|
||||
defp contact_account(username) do
|
||||
user = Pleroma.User.get_cached_by_nickname(username)
|
||||
|
||||
if user do
|
||||
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp configuration do
|
||||
%{
|
||||
accounts: %{
|
||||
|
@ -195,6 +227,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
|
||||
defp configuration2 do
|
||||
configuration()
|
||||
|> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
|
||||
|> put_in([:statuses, :characters_reserved_per_url], 0)
|
||||
|> Map.merge(%{
|
||||
urls: %{
|
||||
streaming: Pleroma.Web.Endpoint.websocket_url(),
|
||||
|
|
|
@ -21,7 +21,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
|
|||
votes_count: votes_count,
|
||||
voters_count: voters_count(object),
|
||||
options: options,
|
||||
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
||||
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]),
|
||||
pleroma: %{
|
||||
non_anonymous: object.data["nonAnonymous"] || false
|
||||
}
|
||||
}
|
||||
|
||||
if params[:for] do
|
||||
|
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.PleromaAPI.EmojiReactionController
|
||||
alias Pleroma.Web.RichMedia.Card
|
||||
|
||||
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
|
||||
|
||||
|
@ -29,9 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
# pagination is restricted to 40 activities at a time
|
||||
defp fetch_rich_media_for_activities(activities) do
|
||||
Enum.each(activities, fn activity ->
|
||||
spawn(fn ->
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
end)
|
||||
spawn(fn -> Card.get_by_activity(activity) end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -113,9 +112,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
|
||||
activities = Enum.filter(opts.activities, & &1)
|
||||
|
||||
# Start fetching rich media before doing anything else, so that later calls to get the cards
|
||||
# only block for timeout in the worst case, as opposed to
|
||||
# length(activities_with_links) * timeout
|
||||
# Start prefetching rich media before doing anything else
|
||||
fetch_rich_media_for_activities(activities)
|
||||
replied_to_activities = get_replied_to_activities(activities)
|
||||
quoted_activities = get_quoted_activities(activities)
|
||||
|
@ -184,7 +181,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
|
||||
|
||||
bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
|
||||
bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
|
||||
|
||||
bookmark_folder =
|
||||
if bookmark != nil do
|
||||
bookmark.folder_id
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
mentions =
|
||||
activity.recipients
|
||||
|
@ -213,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
favourites_count: 0,
|
||||
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
|
||||
favourited: present?(favorited),
|
||||
bookmarked: present?(bookmarked),
|
||||
bookmarked: present?(bookmark),
|
||||
muted: false,
|
||||
pinned: pinned?,
|
||||
sensitive: false,
|
||||
|
@ -227,7 +231,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
emojis: [],
|
||||
pleroma: %{
|
||||
local: activity.local,
|
||||
pinned_at: pinned_at
|
||||
pinned_at: pinned_at,
|
||||
bookmark_folder: bookmark_folder
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -264,7 +269,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
|
||||
|
||||
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
|
||||
bookmark = Activity.get_bookmark(activity, opts[:for])
|
||||
|
||||
bookmark_folder =
|
||||
if bookmark != nil do
|
||||
bookmark.folder_id
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
|
||||
|
||||
|
@ -349,7 +361,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
summary = object.data["summary"] || ""
|
||||
|
||||
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
|
||||
card =
|
||||
case Card.get_by_activity(activity) do
|
||||
%Card{} = result -> render("card.json", result)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
url =
|
||||
if user.local do
|
||||
|
@ -418,7 +434,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
favourites_count: like_count,
|
||||
reblogged: reblogged?(activity, opts[:for]),
|
||||
favourited: present?(favorited),
|
||||
bookmarked: present?(bookmarked),
|
||||
bookmarked: present?(bookmark),
|
||||
muted: muted,
|
||||
pinned: pinned?,
|
||||
sensitive: sensitive,
|
||||
|
@ -448,7 +464,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
emoji_reactions: emoji_reactions,
|
||||
parent_visible: visible_for_user?(reply_to, opts[:for]),
|
||||
pinned_at: pinned_at,
|
||||
quotes_count: object.data["quotesCount"] || 0
|
||||
quotes_count: object.data["quotesCount"] || 0,
|
||||
bookmark_folder: bookmark_folder
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -551,15 +568,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
||||
page_url_data = URI.parse(page_url)
|
||||
|
||||
page_url_data =
|
||||
if is_binary(rich_media["url"]) do
|
||||
URI.merge(page_url_data, URI.parse(rich_media["url"]))
|
||||
else
|
||||
page_url_data
|
||||
end
|
||||
def render("card.json", %Card{fields: rich_media}) do
|
||||
page_url_data = URI.parse(rich_media["url"])
|
||||
|
||||
page_url = page_url_data |> to_string
|
||||
|
||||
|
@ -573,6 +583,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
|
||||
url: page_url,
|
||||
image: image_url,
|
||||
image_description: rich_media["image:alt"] || "",
|
||||
title: rich_media["title"] || "",
|
||||
description: rich_media["description"] || "",
|
||||
pleroma: %{
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
# Note: scope not present in Mastodon: read:bookmarks
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index)
|
||||
|
||||
# Note: scope not present in Mastodon: write:bookmarks
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete]
|
||||
)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
def index(%{assigns: %{user: user}} = conn, _params) do
|
||||
with folders <- BookmarkFolder.for_user(user.id) do
|
||||
conn
|
||||
|> render("index.json", %{folders: folders, as: :folder})
|
||||
end
|
||||
end
|
||||
|
||||
def create(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
|
||||
_
|
||||
) do
|
||||
with {:ok, folder} <- BookmarkFolder.create(user.id, params[:name], params[:emoji]) do
|
||||
render(conn, "show.json", folder: folder)
|
||||
end
|
||||
end
|
||||
|
||||
def update(
|
||||
%{
|
||||
assigns: %{user: user},
|
||||
private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
|
||||
} = conn,
|
||||
_
|
||||
) do
|
||||
with true <- BookmarkFolder.belongs_to_user?(id, user.id),
|
||||
{:ok, folder} <- BookmarkFolder.update(id, params[:name], params[:emoji]) do
|
||||
render(conn, "show.json", folder: folder)
|
||||
else
|
||||
false -> {:error, :forbidden}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(
|
||||
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
|
||||
_
|
||||
) do
|
||||
with true <- BookmarkFolder.belongs_to_user?(id, user.id),
|
||||
{:ok, folder} <- BookmarkFolder.delete(id) do
|
||||
render(conn, "show.json", folder: folder)
|
||||
else
|
||||
false -> {:error, :forbidden}
|
||||
end
|
||||
end
|
||||
end
|
42
lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
Normal file
42
lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.BookmarkFolder
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
def render("show.json", %{folder: %BookmarkFolder{} = folder}) do
|
||||
%{
|
||||
id: folder.id |> to_string(),
|
||||
name: folder.name,
|
||||
emoji: folder.emoji,
|
||||
emoji_url: get_emoji_url(folder.emoji)
|
||||
}
|
||||
end
|
||||
|
||||
def render("index.json", %{folders: folders} = opts) do
|
||||
render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders))
|
||||
end
|
||||
|
||||
defp get_emoji_url(nil) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp get_emoji_url(emoji) do
|
||||
if Emoji.unicode?(emoji) do
|
||||
nil
|
||||
else
|
||||
emoji = Emoji.get(emoji)
|
||||
|
||||
if emoji != nil do
|
||||
Endpoint.url() |> URI.merge(emoji.file) |> to_string()
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.RichMedia.Card
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
|
@ -23,6 +24,12 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
|
|||
}
|
||||
}
|
||||
) do
|
||||
card =
|
||||
case Card.get_by_object(object) do
|
||||
%Card{} = card_data -> StatusView.render("card.json", card_data)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
%{
|
||||
id: id |> to_string(),
|
||||
content: chat_message["content"],
|
||||
|
@ -34,11 +41,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
|
|||
chat_message["attachment"] &&
|
||||
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
|
||||
unread: unread,
|
||||
card:
|
||||
StatusView.render(
|
||||
"card.json",
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
|
||||
)
|
||||
card: card
|
||||
}
|
||||
|> put_idempotency_key()
|
||||
end
|
||||
|
|
101
lib/pleroma/web/rich_media/backfill.ex
Normal file
101
lib/pleroma/web/rich_media/backfill.ex
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Backfill do
|
||||
alias Pleroma.Web.RichMedia.Card
|
||||
alias Pleroma.Web.RichMedia.Parser
|
||||
alias Pleroma.Web.RichMedia.Parser.TTL
|
||||
alias Pleroma.Workers.RichMediaExpirationWorker
|
||||
|
||||
require Logger
|
||||
|
||||
@backfiller Pleroma.Config.get([__MODULE__, :provider], Pleroma.Web.RichMedia.Backfill.Task)
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
@max_attempts 3
|
||||
@retry 5_000
|
||||
|
||||
def start(%{url: url} = args) when is_binary(url) do
|
||||
url_hash = Card.url_to_hash(url)
|
||||
|
||||
args =
|
||||
args
|
||||
|> Map.put(:attempt, 1)
|
||||
|> Map.put(:url_hash, url_hash)
|
||||
|
||||
@backfiller.run(args)
|
||||
end
|
||||
|
||||
def run(%{url: url, url_hash: url_hash, attempt: attempt} = args)
|
||||
when attempt <= @max_attempts do
|
||||
case Parser.parse(url) do
|
||||
{:ok, fields} ->
|
||||
{:ok, card} = Card.create(url, fields)
|
||||
|
||||
maybe_schedule_expiration(url, fields)
|
||||
|
||||
if Map.has_key?(args, :activity_id) do
|
||||
stream_update(args)
|
||||
end
|
||||
|
||||
warm_cache(url_hash, card)
|
||||
|
||||
{:error, {:invalid_metadata, fields}} ->
|
||||
Logger.debug("Rich media incomplete or invalid metadata for #{url}: #{inspect(fields)}")
|
||||
negative_cache(url_hash)
|
||||
|
||||
{:error, :body_too_large} ->
|
||||
Logger.error("Rich media error for #{url}: :body_too_large")
|
||||
negative_cache(url_hash)
|
||||
|
||||
{:error, {:content_type, type}} ->
|
||||
Logger.debug("Rich media error for #{url}: :content_type is #{type}")
|
||||
negative_cache(url_hash)
|
||||
|
||||
e ->
|
||||
Logger.debug("Rich media error for #{url}: #{inspect(e)}")
|
||||
|
||||
:timer.sleep(@retry * attempt)
|
||||
|
||||
run(%{args | attempt: attempt + 1})
|
||||
end
|
||||
end
|
||||
|
||||
def run(%{url: url, url_hash: url_hash}) do
|
||||
Logger.debug("Rich media failure for #{url}")
|
||||
|
||||
negative_cache(url_hash, :timer.minutes(15))
|
||||
end
|
||||
|
||||
defp maybe_schedule_expiration(url, fields) do
|
||||
case TTL.process(fields, url) do
|
||||
{:ok, ttl} when is_number(ttl) ->
|
||||
timestamp = DateTime.from_unix!(ttl)
|
||||
|
||||
RichMediaExpirationWorker.new(%{"url" => url}, scheduled_at: timestamp)
|
||||
|> Oban.insert()
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp stream_update(%{activity_id: activity_id}) do
|
||||
Pleroma.Activity.get_by_id(activity_id)
|
||||
|> Pleroma.Activity.normalize()
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.stream_out()
|
||||
end
|
||||
|
||||
defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val)
|
||||
defp negative_cache(key, ttl \\ nil), do: @cachex.put(:rich_media_cache, key, nil, ttl: ttl)
|
||||
end
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Backfill.Task do
|
||||
alias Pleroma.Web.RichMedia.Backfill
|
||||
|
||||
def run(args) do
|
||||
Task.Supervisor.start_child(Pleroma.TaskSupervisor, Backfill, :run, [args],
|
||||
name: {:global, {:rich_media, args.url_hash}}
|
||||
)
|
||||
end
|
||||
end
|
157
lib/pleroma/web/rich_media/card.ex
Normal file
157
lib/pleroma/web/rich_media/card.ex
Normal file
|
@ -0,0 +1,157 @@
|
|||
defmodule Pleroma.Web.RichMedia.Card do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.RichMedia.Backfill
|
||||
alias Pleroma.Web.RichMedia.Parser
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
schema "rich_media_card" do
|
||||
field(:url_hash, :binary)
|
||||
field(:fields, :map)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(card, attrs) do
|
||||
card
|
||||
|> cast(attrs, [:url_hash, :fields])
|
||||
|> validate_required([:url_hash, :fields])
|
||||
|> unique_constraint(:url_hash)
|
||||
end
|
||||
|
||||
@spec create(String.t(), map()) :: {:ok, t()}
|
||||
def create(url, fields) do
|
||||
url_hash = url_to_hash(url)
|
||||
|
||||
fields = Map.put_new(fields, "url", url)
|
||||
|
||||
%__MODULE__{}
|
||||
|> changeset(%{url_hash: url_hash, fields: fields})
|
||||
|> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
|
||||
end
|
||||
|
||||
@spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
|
||||
def delete(url) do
|
||||
url_hash = url_to_hash(url)
|
||||
@cachex.del(:rich_media_cache, url_hash)
|
||||
|
||||
case get_by_url(url) do
|
||||
%__MODULE__{} = card -> Repo.delete(card)
|
||||
nil -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_by_url(String.t() | nil) :: t() | nil | :error
|
||||
def get_by_url(url) when is_binary(url) do
|
||||
if @config_impl.get([:rich_media, :enabled]) do
|
||||
url_hash = url_to_hash(url)
|
||||
|
||||
@cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
|
||||
result =
|
||||
__MODULE__
|
||||
|> where(url_hash: ^url_hash)
|
||||
|> Repo.one()
|
||||
|
||||
case result do
|
||||
%__MODULE__{} = card -> {:commit, card}
|
||||
_ -> {:ignore, nil}
|
||||
end
|
||||
end)
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def get_by_url(nil), do: nil
|
||||
|
||||
@spec get_or_backfill_by_url(String.t(), map()) :: t() | nil
|
||||
def get_or_backfill_by_url(url, backfill_opts \\ %{}) do
|
||||
case get_by_url(url) do
|
||||
%__MODULE__{} = card ->
|
||||
card
|
||||
|
||||
nil ->
|
||||
backfill_opts = Map.put(backfill_opts, :url, url)
|
||||
|
||||
Backfill.start(backfill_opts)
|
||||
|
||||
nil
|
||||
|
||||
:error ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_by_object(Object.t()) :: t() | nil | :error
|
||||
def get_by_object(object) do
|
||||
case HTML.extract_first_external_url_from_object(object) do
|
||||
nil -> nil
|
||||
url -> get_or_backfill_by_url(url)
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_by_activity(Activity.t()) :: t() | nil | :error
|
||||
# Fake/Draft activity
|
||||
def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do
|
||||
with %Object{} = object <- Object.normalize(activity, fetch: false),
|
||||
url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do
|
||||
case get_by_url(url) do
|
||||
# Cache hit
|
||||
%__MODULE__{} = card ->
|
||||
card
|
||||
|
||||
# Cache miss, but fetch for rendering the Draft
|
||||
_ ->
|
||||
with {:ok, fields} <- Parser.parse(url),
|
||||
{:ok, card} <- create(url, fields) do
|
||||
card
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_by_activity(activity) do
|
||||
with %Object{} = object <- Object.normalize(activity, fetch: false),
|
||||
{_, nil} <- {:cached, get_cached_url(object, activity.id)} do
|
||||
nil
|
||||
else
|
||||
{:cached, url} ->
|
||||
get_or_backfill_by_url(url, %{activity_id: activity.id})
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@spec url_to_hash(String.t()) :: String.t()
|
||||
def url_to_hash(url) do
|
||||
:crypto.hash(:sha256, url) |> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
defp get_cached_url(object, activity_id) do
|
||||
key = "URL|#{activity_id}"
|
||||
|
||||
@cachex.fetch!(:scrubber_cache, key, fn _ ->
|
||||
url = HTML.extract_first_external_url_from_object(object)
|
||||
Activity.HTML.add_cache_key_for(activity_id, key)
|
||||
|
||||
{:commit, url}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -3,65 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Helpers do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.RichMedia.Parser
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
|
||||
|
||||
@options [
|
||||
pool: :media,
|
||||
max_body: 2_000_000,
|
||||
recv_timeout: 2_000
|
||||
]
|
||||
|
||||
def fetch_data_for_object(object) do
|
||||
with true <- @config_impl.get([:rich_media, :enabled]),
|
||||
{:ok, page_url} <-
|
||||
HTML.extract_first_external_url_from_object(object),
|
||||
{:ok, rich_media} <- Parser.parse(page_url) do
|
||||
%{page_url: page_url, rich_media: rich_media}
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||
with true <- @config_impl.get([:rich_media, :enabled]),
|
||||
%Object{} = object <- Object.normalize(activity, fetch: false) do
|
||||
if object.data["fake"] do
|
||||
fetch_data_for_object(object)
|
||||
else
|
||||
key = "URL|#{activity.id}"
|
||||
|
||||
@cachex.fetch!(:scrubber_cache, key, fn _ ->
|
||||
result = fetch_data_for_object(object)
|
||||
|
||||
cond do
|
||||
match?(%{page_url: _, rich_media: _}, result) ->
|
||||
Activity.HTML.add_cache_key_for(activity.id, key)
|
||||
{:commit, result}
|
||||
|
||||
true ->
|
||||
{:ignore, %{}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_data_for_activity(_), do: %{}
|
||||
alias Pleroma.Config
|
||||
|
||||
def rich_media_get(url) do
|
||||
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
|
||||
|
||||
head_check =
|
||||
case Pleroma.HTTP.head(url, headers, @options) do
|
||||
case Pleroma.HTTP.head(url, headers, http_options()) do
|
||||
# If the HEAD request didn't reach the server for whatever reason,
|
||||
# we assume the GET that comes right after won't either
|
||||
{:error, _} = e ->
|
||||
|
@ -76,7 +24,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
|||
:ok
|
||||
end
|
||||
|
||||
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options)
|
||||
with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, http_options())
|
||||
end
|
||||
|
||||
defp check_content_type(headers) do
|
||||
|
@ -92,12 +40,13 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
|||
end
|
||||
end
|
||||
|
||||
@max_body @options[:max_body]
|
||||
defp check_content_length(headers) do
|
||||
max_body = Keyword.get(http_options(), :max_body)
|
||||
|
||||
case List.keyfind(headers, "content-length", 0) do
|
||||
{_, maybe_content_length} ->
|
||||
case Integer.parse(maybe_content_length) do
|
||||
{content_length, ""} when content_length <= @max_body -> :ok
|
||||
{content_length, ""} when content_length <= max_body -> :ok
|
||||
{_, ""} -> {:error, :body_too_large}
|
||||
_ -> :ok
|
||||
end
|
||||
|
@ -106,4 +55,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
|||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp http_options do
|
||||
[
|
||||
pool: :media,
|
||||
max_body: Config.get([:rich_media, :max_body], 5_000_000)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,134 +5,28 @@
|
|||
defmodule Pleroma.Web.RichMedia.Parser do
|
||||
require Logger
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
|
||||
|
||||
defp parsers do
|
||||
Pleroma.Config.get([:rich_media, :parsers])
|
||||
end
|
||||
|
||||
def parse(nil), do: {:error, "No URL provided"}
|
||||
def parse(nil), do: nil
|
||||
|
||||
@spec parse(String.t()) :: {:ok, map()} | {:error, any()}
|
||||
def parse(url) do
|
||||
with :ok <- validate_page_url(url),
|
||||
{:ok, data} <- get_cached_or_parse(url),
|
||||
{:ok, _} <- set_ttl_based_on_image(data, url) do
|
||||
{:ok, data} <- parse_url(url) do
|
||||
data = Map.put(data, "url", url)
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_cached_or_parse(url) do
|
||||
case @cachex.fetch(:rich_media_cache, url, fn ->
|
||||
case parse_url(url) do
|
||||
{:ok, _} = res ->
|
||||
{:commit, res}
|
||||
|
||||
{:error, reason} = e ->
|
||||
# Unfortunately we have to log errors here, instead of doing that
|
||||
# along with ttl setting at the bottom. Otherwise we can get log spam
|
||||
# if more than one process was waiting for the rich media card
|
||||
# while it was generated. Ideally we would set ttl here as well,
|
||||
# so we don't override it number_of_waiters_on_generation
|
||||
# times, but one, obviously, can't set ttl for not-yet-created entry
|
||||
# and Cachex doesn't support returning ttl from the fetch callback.
|
||||
log_error(url, reason)
|
||||
{:commit, e}
|
||||
end
|
||||
end) do
|
||||
{action, res} when action in [:commit, :ok] ->
|
||||
case res do
|
||||
{:ok, _data} = res ->
|
||||
res
|
||||
|
||||
{:error, reason} = e ->
|
||||
if action == :commit, do: set_error_ttl(url, reason)
|
||||
e
|
||||
end
|
||||
|
||||
{:error, e} ->
|
||||
{:error, {:cachex_error, e}}
|
||||
end
|
||||
end
|
||||
|
||||
defp set_error_ttl(_url, :body_too_large), do: :ok
|
||||
defp set_error_ttl(_url, {:content_type, _}), do: :ok
|
||||
|
||||
# The TTL is not set for the errors above, since they are unlikely to change
|
||||
# with time
|
||||
|
||||
defp set_error_ttl(url, _reason) do
|
||||
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
|
||||
@cachex.expire(:rich_media_cache, url, ttl)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp log_error(url, {:invalid_metadata, data}) do
|
||||
Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
|
||||
end
|
||||
|
||||
defp log_error(url, reason) do
|
||||
Logger.warning(fn -> "Rich media error for #{url}: #{inspect(reason)}" end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the rich media cache based on the expiration time of image.
|
||||
|
||||
Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
|
||||
|
||||
## Example
|
||||
|
||||
defmodule MyModule do
|
||||
@behaviour Pleroma.Web.RichMedia.Parser.TTL
|
||||
def ttl(data, url) do
|
||||
image_url = Map.get(data, :image)
|
||||
# do some parsing in the url and get the ttl of the image
|
||||
# and return ttl is unix time
|
||||
parse_ttl_from_url(image_url)
|
||||
end
|
||||
end
|
||||
|
||||
Define the module in the config
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
ttl_setters: [MyModule]
|
||||
"""
|
||||
@spec set_ttl_based_on_image(map(), String.t()) ::
|
||||
{:ok, integer() | :noop} | {:error, :no_key}
|
||||
def set_ttl_based_on_image(data, url) do
|
||||
case get_ttl_from_image(data, url) do
|
||||
ttl when is_number(ttl) ->
|
||||
ttl = ttl * 1000
|
||||
|
||||
case @cachex.expire_at(:rich_media_cache, url, ttl) do
|
||||
{:ok, true} -> {:ok, ttl}
|
||||
{:ok, false} -> {:error, :no_key}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, :noop}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_ttl_from_image(data, url) do
|
||||
[:rich_media, :ttl_setters]
|
||||
|> Pleroma.Config.get()
|
||||
|> Enum.reduce({:ok, nil}, fn
|
||||
module, {:ok, _ttl} ->
|
||||
module.ttl(data, url)
|
||||
|
||||
_, error ->
|
||||
error
|
||||
end)
|
||||
end
|
||||
|
||||
def parse_url(url) do
|
||||
defp parse_url(url) do
|
||||
with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url),
|
||||
{:ok, html} <- Floki.parse_document(html) do
|
||||
html
|
||||
|> maybe_parse()
|
||||
|> Map.put("url", url)
|
||||
|> clean_parsed_data()
|
||||
|> check_parsed_data()
|
||||
end
|
||||
|
|
|
@ -4,4 +4,17 @@
|
|||
|
||||
defmodule Pleroma.Web.RichMedia.Parser.TTL do
|
||||
@callback ttl(map(), String.t()) :: integer() | nil
|
||||
|
||||
@spec process(map(), String.t()) :: {:ok, integer() | nil}
|
||||
def process(data, url) do
|
||||
[:rich_media, :ttl_setters]
|
||||
|> Pleroma.Config.get()
|
||||
|> Enum.reduce_while({:ok, nil}, fn
|
||||
module, acc ->
|
||||
case module.ttl(data, url) do
|
||||
ttl when is_number(ttl) -> {:halt, {:ok, ttl}}
|
||||
_ -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|
|||
|
||||
@impl true
|
||||
def ttl(data, _url) do
|
||||
image = Map.get(data, :image)
|
||||
image = Map.get(data, "image")
|
||||
|
||||
if aws_signed_url?(image) do
|
||||
image
|
||||
|
@ -15,14 +15,15 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|
|||
|> format_query_params()
|
||||
|> get_expiration_timestamp()
|
||||
else
|
||||
{:error, "Not aws signed url #{inspect(image)}"}
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp aws_signed_url?(image) when is_binary(image) and image != "" do
|
||||
%URI{host: host, query: query} = URI.parse(image)
|
||||
|
||||
String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires")
|
||||
is_binary(host) and String.contains?(host, "amazonaws.com") and
|
||||
String.contains?(query, "X-Amz-Expires")
|
||||
end
|
||||
|
||||
defp aws_signed_url?(_), do: nil
|
||||
|
|
20
lib/pleroma/web/rich_media/parser/ttl/opengraph.ex
Normal file
20
lib/pleroma/web/rich_media/parser/ttl/opengraph.ex
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Parser.TTL.Opengraph do
|
||||
@behaviour Pleroma.Web.RichMedia.Parser.TTL
|
||||
|
||||
@impl true
|
||||
def ttl(%{"ttl" => ttl_string}, _url) when is_binary(ttl_string) do
|
||||
try do
|
||||
ttl = String.to_integer(ttl_string)
|
||||
now = DateTime.utc_now() |> DateTime.to_unix()
|
||||
now + ttl
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def ttl(_, _), do: nil
|
||||
end
|
|
@ -292,6 +292,11 @@ defmodule Pleroma.Web.Router do
|
|||
post("/frontends/install", FrontendController, :install)
|
||||
|
||||
post("/backups", AdminAPIController, :create_backup)
|
||||
|
||||
get("/rules", RuleController, :index)
|
||||
post("/rules", RuleController, :create)
|
||||
patch("/rules/:id", RuleController, :update)
|
||||
delete("/rules/:id", RuleController, :delete)
|
||||
end
|
||||
|
||||
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
|
@ -580,6 +585,11 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/backups", BackupController, :index)
|
||||
post("/backups", BackupController, :create)
|
||||
|
||||
get("/bookmark_folders", BookmarkFolderController, :index)
|
||||
post("/bookmark_folders", BookmarkFolderController, :create)
|
||||
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
|
||||
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
|
@ -759,11 +769,11 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/instance", InstanceController, :show)
|
||||
get("/instance/peers", InstanceController, :peers)
|
||||
get("/instance/rules", InstanceController, :rules)
|
||||
|
||||
get("/statuses", StatusController, :index)
|
||||
get("/statuses/:id", StatusController, :show)
|
||||
get("/statuses/:id/context", StatusController, :context)
|
||||
get("/statuses/:id/card", StatusController, :card)
|
||||
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
|
||||
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
|
||||
get("/statuses/:id/history", StatusController, :show_history)
|
||||
|
|
|
@ -40,6 +40,11 @@ defmodule Pleroma.Workers.BackgroundWorker do
|
|||
Pleroma.FollowingRelationship.move_following(origin, target)
|
||||
end
|
||||
|
||||
def perform(%Job{args: %{"op" => "verify_fields_links", "user_id" => user_id}}) do
|
||||
user = User.get_by_id(user_id)
|
||||
User.perform(:verify_fields_links, user)
|
||||
end
|
||||
|
||||
def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
|
||||
Instance.perform(:delete_instance, host)
|
||||
end
|
||||
|
|
|
@ -52,7 +52,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
|||
{:error, {:reject, reason}} -> {:cancel, reason}
|
||||
{:signature, false} -> {:cancel, :invalid_signature}
|
||||
{:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason}
|
||||
e -> e
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
15
lib/pleroma/workers/rich_media_expiration_worker.ex
Normal file
15
lib/pleroma/workers/rich_media_expiration_worker.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.RichMediaExpirationWorker do
|
||||
alias Pleroma.Web.RichMedia.Card
|
||||
|
||||
use Oban.Worker,
|
||||
queue: :rich_media_expiration
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"url" => url} = _args}) do
|
||||
Card.delete(url)
|
||||
end
|
||||
end
|
12
priv/repo/migrations/20220203224011_create_rules.exs
Normal file
12
priv/repo/migrations/20220203224011_create_rules.exs
Normal file
|
@ -0,0 +1,12 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateRules do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:rules) do
|
||||
add(:priority, :integer, default: 0, null: false)
|
||||
add(:text, :text, null: false)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateRichMediaCard do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:rich_media_card) do
|
||||
add(:url_hash, :bytea)
|
||||
add(:fields, :map)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create(unique_index(:rich_media_card, [:url_hash]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.CreateBookmarkFolders do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:bookmark_folders, primary_key: false) do
|
||||
add(:id, :uuid, primary_key: true)
|
||||
add(:name, :string, null: false)
|
||||
add(:emoji, :string)
|
||||
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
alter table(:bookmarks) do
|
||||
add_if_not_exists(
|
||||
:folder_id,
|
||||
references(:bookmark_folders, type: :uuid, on_delete: :nilify_all)
|
||||
)
|
||||
end
|
||||
|
||||
create_if_not_exists(unique_index(:bookmark_folders, [:user_id, :name]))
|
||||
end
|
||||
end
|
13
priv/repo/migrations/20240406000000_add_hint_to_rules.exs
Normal file
13
priv/repo/migrations/20240406000000_add_hint_to_rules.exs
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.AddHintToRules do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:rules) do
|
||||
add_if_not_exists(:hint, :text)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,6 +2,7 @@
|
|||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://purl.archive.org/socialweb/webfinger",
|
||||
{
|
||||
"Emoji": "toot:Emoji",
|
||||
"Hashtag": "as:Hashtag",
|
||||
|
@ -40,7 +41,9 @@
|
|||
"@type": "@id"
|
||||
},
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||
"formerRepresentations": "litepub:formerRepresentations"
|
||||
"formerRepresentations": "litepub:formerRepresentations",
|
||||
"sm": "http://smithereen.software/ns#",
|
||||
"nonAnonymous": "sm:nonAnonymous"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
1
test/fixtures/minds-invalid-mention-post.json
vendored
Normal file
1
test/fixtures/minds-invalid-mention-post.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}}
|
1
test/fixtures/minds-pleroma-mentioned-post.json
vendored
Normal file
1
test/fixtures/minds-pleroma-mentioned-post.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
|
392
test/fixtures/rich_media/reddit.html
vendored
Normal file
392
test/fixtures/rich_media/reddit.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json
vendored
Normal file
1
test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"type":"Question","id":"https://friends.grishka.me/posts/54642","attributedTo":"https://friends.grishka.me/users/1","content":"<p>здесь тоже можно что-то написать отдельно от опроса</p>","published":"2021-09-04T00:22:16Z","url":"https://friends.grishka.me/posts/54642","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://friends.grishka.me/users/1/followers"],"replies":{"type":"Collection","id":"https://friends.grishka.me/posts/54642/replies","first":{"type":"CollectionPage","items":[],"partOf":"https://friends.grishka.me/posts/54642/replies","next":"https://friends.grishka.me/posts/54642/replies?page=1"}},"sensitive":false,"likes":"https://friends.grishka.me/posts/54642/likes","name":"тестовый опрос","oneOf":[{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/76","name":"тестовый ответ 1","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/76/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/77","name":"тестовый ответ 2","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/77/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/78","name":"тестовый ответ 3","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/78/votes","totalItems":6,"items":[]}}],"votersCount":14,"nonAnonymous":true,"@context":["https://www.w3.org/ns/activitystreams",{"sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#","sm":"http://smithereen.software/ns#","votersCount":"toot:votersCount","nonAnonymous":"sm:nonAnonymous"}]}
|
1
test/fixtures/tesla_mock/smithereen_user.json
vendored
Normal file
1
test/fixtures/tesla_mock/smithereen_user.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"type":"Person","id":"https://friends.grishka.me/users/1","name":"Григорий Клюшников","icon":{"type":"Image","image":{"type":"Image","url":"https://friends.grishka.me/i/6QLsOws97AWp5N_osd74C1IC1ijnFopyCBD9MSEeXNQ/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg","width":1280,"height":960},"width":573,"height":572,"cropRegion":[0.26422762870788574,0.3766937553882599,0.7113820910453796,0.9728997349739075],"url":"https://friends.grishka.me/i/ql_49PQcETAWgY_nC-Qj63H_Oa6FyOAEoWFkUSSkUvQ/c:573:572:nowe:338:362/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg"},"summary":"<p>Делаю эту хрень, пытаюсь вырвать социальные сети из жадных лап корпораций</p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p>This server does NOT support direct messages. Please write me <a href=\"https://t.me/grishka\">on Telegram</a> or <a href=\"https://matrix.to/#/@grishk:matrix.org\">Matrix</a>.</p>","url":"https://friends.grishka.me/grishka","preferredUsername":"grishka","inbox":"https://friends.grishka.me/users/1/inbox","outbox":"https://friends.grishka.me/users/1/outbox","followers":"https://friends.grishka.me/users/1/followers","following":"https://friends.grishka.me/users/1/following","endpoints":{"sharedInbox":"https://friends.grishka.me/activitypub/sharedInbox","collectionSimpleQuery":"https://friends.grishka.me/users/1/collectionQuery"},"publicKey":{"id":"https://friends.grishka.me/users/1#main-key","owner":"https://friends.grishka.me/users/1","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjlakm+i/d9ER/hIeR7KfiFW+SdLZj2SkKIeM8cmR+YFJuh9ghFqXrkFEjcaqUnAFqe5gYDNSQACnDLA8y4DnzjfGNIohKAnRoa9x6GORmfKQvcnjaTZ53S1NvUiPPyc0Pv/vfCtY/Ab0CEXe5BLqL38oZn817Jf7pBrPRTYH7m012kvwAUTT6k0Y8lPITBEG7nzYbbuGcrN9Y/RDdwE08jmBXlZ45bahRH3VNXVpQE17dCzJB+7k+iJ1R7YCoI+DuMlBYGXGE2KVk46NZTuLnOjFV9SyXfWX4/SrJM4oxev+SX2N75tQgmNZmVVHeqg2ZcbC0WCfNjJOi2HHS9MujwIDAQAB\n-----END PUBLIC KEY-----\n"},"wall":"https://friends.grishka.me/users/1/wall","firstName":"Григорий","lastName":"Клюшников","middleName":"Александрович","vcard:bday":"1993-01-22","gender":"http://schema.org#Male","supportsFriendRequests":true,"friends":"https://friends.grishka.me/users/1/friends","groups":"https://friends.grishka.me/users/1/groups","capabilities":{"supportsFriendRequests":true},"@context":["https://www.w3.org/ns/activitystreams",{"sm":"http://smithereen.software/ns#","cropRegion":{"@id":"sm:cropRegion","@container":"@list"},"wall":{"@id":"sm:wall","@type":"@id"},"collectionSimpleQuery":"sm:collectionSimpleQuery","sc":"http://schema.org#","firstName":"sc:givenName","lastName":"sc:familyName","middleName":"sc:additionalName","gender":{"@id":"sc:gender","@type":"sc:GenderType"},"maidenName":"sm:maidenName","friends":{"@id":"sm:friends","@type":"@id"},"groups":{"@id":"sm:groups","@type":"@id"},"vcard":"http://www.w3.org/2006/vcard/ns#","capabilities":"litepub:capabilities","supportsFriendRequests":"sm:supportsFriendRequests","litepub":"http://litepub.social/ns#"},"https://w3id.org/security/v1"]}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue