Merge remote-tracking branch 'origin/develop' into multitenancy

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-03-23 15:00:36 +01:00
commit 55ba7c3a37
115 changed files with 1677 additions and 503 deletions

5
.gitignore vendored
View file

@ -57,5 +57,6 @@ pleroma.iml
.tool-versions
# Editor temp files
/*~
/*#
*~
*#
*.swp

View file

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

View file

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.6.2
### Security
- MRF StealEmojiPolicy: Sanitize shortcodes (thanks to Hazel K for the report
## 2.6.1
### Changed
- - Document maximum supported version of Erlang & Elixir

View file

View file

@ -0,0 +1 @@
Include following/followers in backups

View file

@ -0,0 +1 @@
Support Bandit as an alternative to Cowboy for the HTTP server.

View file

@ -0,0 +1 @@
Allow to group bookmarks in folders

View file

View file

@ -0,0 +1 @@
Fix federation with Convergence AP Bridge

View file

@ -0,0 +1 @@
- Config: Check the permissions of the linked file instead of the symlink

View file

@ -0,0 +1 @@
MediaProxy was setting the content-length header which is not permitted by RFC9112§6.2 when we are chunking the reply as it conflicts with the existence of the transfer-encoding header.

View file

View file

@ -0,0 +1 @@
Add ForceMention MRF

View file

@ -0,0 +1 @@
Video framegrabs were not working correctly after the change to use Exile to execute ffmpeg

View file

1
changelog.d/gun_pool.fix Normal file
View file

@ -0,0 +1 @@
Fix logic error in Gun connection pooling which prevented retries even when the worker was launched with retry = true

View file

@ -0,0 +1 @@
Connection pool errors when publishing an activity is a soft-error that will be retried shortly.

View file

View file

@ -0,0 +1 @@
Add contact account to InstanceView

View file

View file

@ -0,0 +1 @@
Handle cases when users.inbox is nil.

View file

@ -0,0 +1 @@
Verify profile link ownership with rel="me"

1
changelog.d/memleak.fix Normal file
View file

@ -0,0 +1 @@
Fix a memory leak caused by Websocket connections that would not enter a state where a full garbage collection run could be triggered.

View file

View file

@ -0,0 +1 @@
Fix notifications query which was not using the index properly

View file

@ -0,0 +1 @@
Notifications: improve performance by filtering on users table instead of activities table

View file

@ -0,0 +1 @@
Use User.full_nickname/1 in oauth html template

View file

@ -0,0 +1 @@
Disable jit by default for PostgreSQL

View file

@ -0,0 +1 @@
Expose nonAnonymous field from Smithereen polls

View file

1
changelog.d/tesla.deps Normal file
View file

@ -0,0 +1 @@
Update Tesla HTTP client middleware to 1.8.0

View file

View file

@ -0,0 +1 @@
Set default values on validators for transient objects (attachment, poll options)

View file

@ -0,0 +1 @@
Refactor the Mastodon /api/v1/streaming websocket handler to use Phoenix.Socket.Transport

View file

@ -114,14 +114,7 @@ config :pleroma, :uri_schemes,
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1},
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
]}
]
ip: {127, 0, 0, 1}
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
@ -425,6 +418,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: [],
@ -807,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,

View file

@ -566,6 +566,20 @@ config :pleroma, :config_description, [
"Cool instance"
]
},
%{
key: :status_page,
type: :string,
description: "A page where people can see the status of the server during an outage",
suggestions: [
"https://status.pleroma.example.org"
]
},
%{
key: :contact_username,
type: :string,
description: "Instance owner username",
suggestions: ["admin"]
},
%{
key: :limit,
type: :integer,

View file

@ -8,8 +8,7 @@ import Config
# with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint,
http: [
port: 4000,
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
port: 4000
],
protocol: "http",
debug_errors: true,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,93 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Phoenix.Transports.WebSocket.Raw do
import Plug.Conn,
only: [
fetch_query_params: 1,
send_resp: 3
]
alias Phoenix.Socket.Transport
def default_config do
[
timeout: 60_000,
transport_log: false,
cowboy: Phoenix.Endpoint.CowboyWebSocket
]
end
def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport)
conn =
conn
|> fetch_query_params
|> Transport.transport_log(opts[:transport_log])
|> Transport.check_origin(handler, endpoint, opts)
case conn do
%{halted: false} = conn ->
case handler.connect(%{
endpoint: endpoint,
transport: transport,
options: [serializer: nil],
params: conn.params
}) do
{:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}}
:error ->
send_resp(conn, :forbidden, "")
{:error, conn}
end
_ ->
{:error, conn}
end
end
def init(conn, _) do
send_resp(conn, :bad_request, "")
{:error, conn}
end
def ws_init({socket, config}) do
Process.flag(:trap_exit, true)
{:ok, %{socket: socket}, config[:timeout]}
end
def ws_handle(op, data, state) do
state.socket.handler
|> apply(:handle, [op, data, state])
|> case do
{op, data} ->
{:reply, {op, data}, state}
{op, data, state} ->
{:reply, {op, data}, state}
%{} = state ->
{:ok, state}
_ ->
{:ok, state}
end
end
def ws_info({_, _} = tuple, state) do
{:reply, tuple, state}
end
def ws_info(_tuple, state), do: {:ok, state}
def ws_close(state) do
ws_handle(:closed, :normal, state)
end
def ws_terminate(reason, state) do
ws_handle(:closed, reason, state)
end
end

View file

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

View file

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

View 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

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
with_runtime_config =
if File.exists?(config_path) do
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
%File.Stat{mode: mode} = File.lstat!(config_path)
%File.Stat{mode: mode} = File.stat!(config_path)
if Bitwise.band(mode, 0o007) > 0 do
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"

View file

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

View file

@ -18,10 +18,12 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
)
end
def start_worker(opts, retry \\ false) do
def start_worker(opts, last_attempt \\ false) do
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
{:error, :max_children} ->
if Enum.any?([retry, free_pool()], &match?(&1, :error)) do
funs = [fn -> last_attempt end, fn -> match?(:error, free_pool()) end]
if Enum.any?(funs, fn fun -> fun.() end) do
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
{:error, :pool_full}
else

View file

@ -40,28 +40,32 @@ 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"),
{:ok, env} <- HTTP.get(url, [], pool: :media),
{:ok, pid} <- StringIO.open(env.body) do
body_stream = IO.binstream(pid, 1)
Exile.stream!(
[
executable,
"-i",
"pipe:0",
"-vframes",
"1",
"-f",
"mjpeg",
"pipe:1"
],
input: body_stream,
ignore_epipe: true,
stderr: :disable
)
|> Enum.into(<<>>)
result =
Exile.stream!(
[
executable,
"-i",
"pipe:0",
"-vframes",
"1",
"-f",
"mjpeg",
"pipe:1"
],
input: body_stream,
ignore_epipe: true,
stderr: :disable
)
|> Enum.into(<<>>)
{:ok, result}
else
nil -> {:error, {:ffmpeg, :command_not_found}}
{:error, _} = error -> error

View file

@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do
@ -18,4 +18,17 @@ defmodule Pleroma.Maps do
rescue
_ -> data
end
def filter_empty_values(data) do
# TODO: Change to Map.filter in Elixir 1.13+
data
|> Enum.filter(fn
{_k, nil} -> false
{_k, ""} -> false
{_k, []} -> false
{_k, %{} = v} -> Map.keys(v) != []
{_k, _v} -> true
end)
|> Map.new()
end
end

View file

@ -88,7 +88,7 @@ defmodule Pleroma.Notification do
where: q.seen == true,
select: type(q.id, :string),
limit: 1,
order_by: [desc: :id]
order_by: fragment("? desc nulls last", q.id)
)
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

View file

@ -28,7 +28,7 @@ defmodule Pleroma.Password.Pbkdf2 do
iterations = String.to_integer(iterations)
digest = String.to_atom(digest)
digest = String.to_existing_atom(digest)
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)

View file

@ -8,7 +8,7 @@ defmodule Pleroma.ReverseProxy do
~w(if-unmodified-since if-none-match) ++ @range_headers
@resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++
~w(content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]

View file

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

View file

@ -59,7 +59,7 @@ defmodule Pleroma.Telemetry.Logger do
_,
_
) do
Logger.error(fn ->
Logger.debug(fn ->
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
end)
end
@ -81,7 +81,7 @@ defmodule Pleroma.Telemetry.Logger do
%{key: key, protocol: :http},
_
) do
Logger.info(fn ->
Logger.debug(fn ->
"Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur."
end)
end

View file

@ -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
@ -599,9 +600,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
@ -1227,6 +1242,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
@ -2009,8 +2028,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(%{
@ -2698,10 +2754,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)

View file

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

View 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

View file

@ -36,6 +36,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
extension = if extension == "", do: ".png", else: extension
shortcode = Path.basename(shortcode)
file_path = Path.join(emoji_dir_path, shortcode <> extension)
case File.write(file_path, response.body) do
@ -78,6 +79,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
new_emojis =
foreign_emojis
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
|> Enum.filter(fn {shortcode, _url} ->
reject_emoji? =
[:mrf_steal_emoji, :rejected_shortcodes]

View file

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

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.User
@ -24,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_object_defaults(data) do
data = Maps.filter_empty_values(data)
context =
Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]

View file

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

View file

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

View file

@ -129,6 +129,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
_ -> {:error, e}
end
{:error, :pool_full} ->
Logger.debug("Publisher snoozing worker job due to full connection pool")
{:snooze, 30}
e ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
Logger.metadata(activity: id, inbox: inbox)
@ -154,19 +158,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end
end
defp should_federate?(inbox, public) do
if public do
true
else
%{host: host} = URI.parse(inbox)
def should_federate?(nil, _), do: false
def should_federate?(_, true), do: true
quarantined_instances =
Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
def should_federate?(inbox, _) do
%{host: host} = URI.parse(inbox)
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
quarantined_instances =
Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]

View file

@ -336,10 +336,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_tag(object), do: object
def fix_content_map(%{"contentMap" => nil} = object) do
Map.drop(object, ["contentMap"])
end
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)

View file

@ -138,7 +138,8 @@ defmodule Pleroma.Web.ApiSpec do
"Scheduled statuses",
"Search",
"Status actions",
"Media attachments"
"Media attachments",
"Bookmark folders"
]
},
%{

View file

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

View file

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

View 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

View file

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

View file

@ -9,6 +9,16 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config
socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler,
longpoll: false,
websocket: [
path: "/",
compress: false,
error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
fullsweep_after: 20
]
)
socket("/socket", Pleroma.Web.UserSocket,
websocket: [
path: "/websocket",
@ -18,7 +28,8 @@ defmodule Pleroma.Web.Endpoint do
],
timeout: 60_000,
transport_log: false,
compress: false
compress: false,
fullsweep_after: 20
],
longpoll: false
)
@ -32,7 +43,8 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, no-cache"
@static_cache_control "public, max-age=1209600"
@static_cache_disabled "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@ -43,22 +55,32 @@ defmodule Pleroma.Web.Endpoint do
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
cache_control_for_etags: "public, max-age=1209600",
headers: %{
"cache-control" => "public, max-age=1209600"
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
# Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
only: ["index.html"],
gzip: true,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_disabled
}
)
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
@ -75,9 +97,9 @@ defmodule Pleroma.Web.Endpoint do
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
cache_control_for_etags: @static_cache_control,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_control
"cache-control" => @static_cache_disabled
}
)
@ -92,9 +114,9 @@ defmodule Pleroma.Web.Endpoint do
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_control,
cache_control_for_etags: @static_cache_disabled,
headers: %{
"cache-control" => @static_cache_control
"cache-control" => @static_cache_disabled
}
)

View file

@ -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
@ -411,13 +412,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
@ -573,10 +583,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 =

View file

@ -30,6 +30,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),
@ -65,11 +66,12 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
registrations: %{
enabled: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
message: nil
message: nil,
url: nil
},
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)
@ -80,7 +82,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
%{
title: Keyword.get(instance, :name),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
languages: Keyword.get(instance, :languages, ["en"])
languages: Keyword.get(instance, :languages, ["en"]),
rules: []
}
end
@ -130,7 +133,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
end,
"pleroma:get:main/ostatus",
"pleroma:group_actors",
"multitenancy"
"pleroma:bookmark_folders",
"pleroma:multitenancy"
]
|> Enum.filter(& &1)
end
@ -171,15 +175,35 @@ 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: %{
max_featured_tags: 0
},
statuses: %{
max_characters: Config.get([:instance, :limit]),
max_media_attachments: Config.get([:instance, :max_media_attachments])
},
media_attachments: %{
image_size_limit: Config.get([:instance, :upload_limit]),
video_size_limit: Config.get([:instance, :upload_limit])
video_size_limit: Config.get([:instance, :upload_limit]),
supported_mime_types: ["application/octet-stream"]
},
polls: %{
max_options: Config.get([:instance, :poll_limits, :max_options]),
@ -193,7 +217,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
defp configuration2 do
configuration()
|> Map.merge(%{
urls: %{streaming: Pleroma.Web.Endpoint.websocket_url()}
urls: %{
streaming: Pleroma.Web.Endpoint.websocket_url(),
status: Config.get([:instance, :status_page])
},
vapid: %{
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}
})
end

View file

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

View file

@ -184,7 +184,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
bookmark_folder =
if bookmark != nil do
bookmark.folder_id
else
nil
end
mentions =
activity.recipients
@ -213,7 +220,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: 0,
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
bookmarked: present?(bookmark),
muted: false,
pinned: pinned?,
sensitive: false,
@ -227,7 +234,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emojis: [],
pleroma: %{
local: activity.local,
pinned_at: pinned_at
pinned_at: pinned_at,
bookmark_folder: bookmark_folder
}
}
end
@ -264,7 +272,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
bookmark = Activity.get_bookmark(activity, opts[:for])
bookmark_folder =
if bookmark != nil do
bookmark.folder_id
else
nil
end
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
@ -418,7 +433,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
bookmarked: present?(bookmark),
muted: muted,
pinned: pinned?,
sensitive: sensitive,
@ -448,7 +463,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at,
quotes_count: object.data["quotesCount"] || 0
quotes_count: object.data["quotesCount"] || 0,
bookmark_folder: bookmark_folder
}
}
end

View file

@ -11,28 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
alias Pleroma.Web.Streamer
alias Pleroma.Web.StreamerView
@behaviour :cowboy_websocket
@behaviour Phoenix.Socket.Transport
# Client ping period.
@tick :timer.seconds(30)
# Cowboy timeout period.
@timeout :timer.seconds(60)
# Hibernate every X messages
@hibernate_every 100
def init(%{qs: qs} = req, state) do
with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
access_token <- Map.get(params, "access_token"),
{:ok, user, oauth_token} <- authenticate_request(access_token, sec_websocket),
{:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do
req =
if sec_websocket do
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
else
req
end
@impl Phoenix.Socket.Transport
def child_spec(_opts), do: :ignore
# This only prepares the connection and is not in the process yet
@impl Phoenix.Socket.Transport
def connect(%{params: params} = transport_info) do
with access_token <- Map.get(params, "access_token"),
{:ok, user, oauth_token} <- authenticate_request(access_token),
{:ok, topic} <-
Streamer.get_topic(params["stream"], user, oauth_token, params) do
topics =
if topic do
[topic]
@ -40,41 +33,40 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
[]
end
{:cowboy_websocket, req,
%{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
%{idle_timeout: @timeout}}
state = %{
user: user,
topics: topics,
oauth_token: oauth_token,
count: 0,
timer: nil
}
{:ok, state}
else
{:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
req = :cowboy_req.reply(404, req)
{:ok, req, state}
Logger.debug("#{__MODULE__} bad topic #{inspect(transport_info)}")
{:error, :bad_topic}
{:error, :unauthorized} ->
Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
req = :cowboy_req.reply(401, req)
{:ok, req, state}
Logger.debug("#{__MODULE__} authentication error: #{inspect(transport_info)}")
{:error, :unauthorized}
end
end
def websocket_init(state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
)
# All subscriptions/links and messages cannot be created
# until the processed is launched with init/1
@impl Phoenix.Socket.Transport
def init(state) do
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
{:ok, %{state | timer: timer()}}
Process.send_after(self(), :ping, @tick)
{:ok, state}
end
# Client's Pong frame.
def websocket_handle(:pong, state) do
if state.timer, do: Process.cancel_timer(state.timer)
{:ok, %{state | timer: timer()}}
end
# We only receive pings for now
def websocket_handle(:ping, state), do: {:ok, state}
def websocket_handle({:text, text}, state) do
@impl Phoenix.Socket.Transport
def handle_in({text, [opcode: :text]}, state) do
with {:ok, %{} = event} <- Jason.decode(text) do
handle_client_event(event, state)
else
@ -84,50 +76,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
def websocket_handle(frame, state) do
def handle_in(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
def websocket_info({:render_with_user, view, template, item, topic}, state) do
@impl Phoenix.Socket.Transport
def handle_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
message = view.render(template, item, user, topic)
{:push, {:text, message}, %{state | user: user}}
else
{:ok, state}
end
end
def websocket_info({:text, message}, state) do
# If the websocket processed X messages, force an hibernate/GC.
# We don't hibernate at every message to balance CPU usage/latency with RAM usage.
if state.count > @hibernate_every do
{:reply, {:text, message}, %{state | count: 0}, :hibernate}
else
{:reply, {:text, message}, %{state | count: state.count + 1}}
end
def handle_info({:text, text}, state) do
{:push, {:text, text}, state}
end
# Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
# As we hibernate there, reset the count to 0.
# If the client misses :pong, Cowboy will automatically timeout the connection after
# `@idle_timeout`.
def websocket_info(:tick, state) do
{:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
def handle_info(:ping, state) do
Process.send_after(self(), :ping, @tick)
{:push, {:ping, ""}, state}
end
def websocket_info(:close, state) do
{:stop, state}
def handle_info(:close, state) do
{:stop, {:closed, 'connection closed by server'}, state}
end
# State can be `[]` only in case we terminate before switching to websocket,
# we already log errors for these cases in `init/1`, so just do nothing here
def terminate(_reason, _req, []), do: :ok
def handle_info(msg, state) do
Logger.debug("#{__MODULE__} received info: #{inspect(msg)}")
def terminate(reason, _req, state) do
{:ok, state}
end
@impl Phoenix.Socket.Transport
def terminate(reason, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)})"
)
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
@ -135,16 +124,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
# Public streams without authentication.
defp authenticate_request(nil, nil) do
defp authenticate_request(nil) do
{:ok, nil, nil}
end
# Authenticated streams.
defp authenticate_request(access_token, sec_websocket) do
token = access_token || sec_websocket
with true <- is_bitstring(token),
oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
defp authenticate_request(access_token) do
with oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user, oauth_token}
else
@ -152,36 +138,32 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
defp timer do
Process.send_after(self(), :tick, @tick)
end
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
with {_, {:ok, topic}} <-
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
{_, false} <- {:subscribed, topic in state.topics} do
Streamer.add_socket(topic, state.oauth_token)
{[
{:text,
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
], %{state | topics: [topic | state.topics]}}
message =
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})
{:reply, :ok, {:text, message}, %{state | topics: [topic | state.topics]}}
else
{:subscribed, true} ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})
{:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
type: "subscribe",
result: "error",
error: error
})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{
type: "subscribe",
result: "error",
error: error
})
{:reply, :error, {:text, message}, state}
end
end
@ -191,26 +173,26 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{_, true} <- {:subscribed, topic in state.topics} do
Streamer.remove_socket(topic)
{[
{:text,
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
], %{state | topics: List.delete(state.topics, topic)}}
message =
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})
{:reply, :ok, {:text, message}, %{state | topics: List.delete(state.topics, topic)}}
else
{:subscribed, false} ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})
{:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
type: "unsubscribe",
result: "error",
error: error
})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{
type: "unsubscribe",
result: "error",
error: error
})
{:reply, :error, {:text, message}, state}
end
end
@ -219,39 +201,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
state
) do
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
{:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "success"
})}
], %{state | user: user, oauth_token: oauth_token}}
{:ok, user, oauth_token} <- authenticate_request(access_token) do
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "success"
})
{:reply, :ok, {:text, message}, %{state | user: user, oauth_token: oauth_token}}
else
{:auth, _, _} ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :already_authenticated
})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :already_authenticated
})
{:reply, :error, {:text, message}, state}
_ ->
{[
{:text,
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :unauthorized
})}
], state}
message =
StreamerView.render("pleroma_respond.json", %{
type: "pleroma:authenticate",
result: "error",
error: :unauthorized
})
{:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(params, state) do
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
{[], state}
{:ok, state}
end
def handle_error(conn, :unauthorized) do
Plug.Conn.send_resp(conn, 401, "Unauthorized")
end
def handle_error(conn, _reason) do
Plug.Conn.send_resp(conn, 404, "Not Found")
end
end

View file

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

View 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

View file

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

View file

@ -28,7 +28,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}})
when op in ["blocks_import", "follow_import", "mutes_import"] do
user = User.get_cached_by_id(user_id)
{:ok, User.Import.perform(String.to_atom(op), user, identifiers)}
{:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)}
end
def perform(%Job{
@ -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

View file

@ -22,8 +22,11 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do
{:error, :allowed_depth} ->
{:discard, :allowed_depth}
_ ->
:error
{:error, _} = e ->
e
e ->
{:error, e}
end
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: version("2.6.51"),
version: version("2.6.52"),
elixir: "~> 1.11",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
@ -136,7 +136,7 @@ defmodule Pleroma.Mixfile do
{:calendar, "~> 1.0"},
{:cachex, "~> 3.2"},
{:poison, "~> 3.0", override: true},
{:tesla, "~> 1.4.0", override: true},
{:tesla, "~> 1.8.0"},
{:castore, "~> 0.1"},
{:cowlib, "~> 2.9", override: true},
{:gun, "~> 2.0.0-rc.1", override: true},
@ -185,8 +185,9 @@ defmodule Pleroma.Mixfile do
{:elixir_make, "~> 0.7.7", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
{:exile,
git: "https://git.pleroma.social/pleroma/elixir-libraries/exile.git",
ref: "0d6337cf68e7fbc8a093cae000955aa93b067f91"},
git: "https://github.com/akash-akya/exile.git",
ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"},
{:bandit, "~> 1.2"},
## dev & test
{:ex_doc, "~> 0.22", only: :dev, runtime: false},

View file

@ -1,5 +1,6 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"bandit": {:hex, :bandit, "1.2.1", "aa485b4ac175065b8e0fb5864ddd5dd7b50d52336b36f61c82f484c3718b3d15", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27393e590a407f1b7d51c5fee4737f139fe224a30449ce25061eac70f763896b"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
@ -46,12 +47,12 @@
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"exile": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/exile.git", "0d6337cf68e7fbc8a093cae000955aa93b067f91", [ref: "0d6337cf68e7fbc8a093cae000955aa93b067f91"]},
"exile": {:git, "https://github.com/akash-akya/exile.git", "be87c33b02a7c3c5d22d2ece01fbd462355b28ef", [ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"]},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
"fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
@ -132,7 +133,8 @@
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.0", "b583c3f18508f5c5561b674d16cf5d9afd2ea3c04505b7d92baaeac93c1b8260", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "9cba950e1c4733468efbe3f821841f34ac05d28e7af7798622f88ecdbbe63ea3"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"},
"thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},

View file

@ -3,16 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-18 09:07+0000\n"
"PO-Revision-Date: 2021-09-19 09:45+0000\n"
"Last-Translator: Ryo Ueno <r.ueno.nfive@gmail.com>\n"
"PO-Revision-Date: 2024-02-16 11:49+0000\n"
"Last-Translator: SyoBoN <syobon@syobon.net>\n"
"Language-Team: Japanese <https://translate.pleroma.social/projects/pleroma/"
"pleroma/ja/>\n"
"pleroma-backend-domain-errors/ja/>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.6.2\n"
"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
@ -25,11 +25,11 @@ msgstr ""
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
msgstr "空にすることはできません"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
msgstr "すでに使用されています"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
@ -37,7 +37,7 @@ msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
msgstr "書式が正しくありません"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
@ -45,7 +45,7 @@ msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
msgstr "予約されています"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
@ -61,23 +61,23 @@ msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[0] "%{count}文字である必要があります"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[0] "%{count}個の要素を含む必要があります"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[0] "%{count}文字以上である必要があります"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[0] "%{count}個以上の要素を含む必要があります"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[0] "%{count}文字以下である必要があります"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
@ -102,7 +102,7 @@ msgstr "%{number}でなければなりません"
#: lib/pleroma/web/common_api/common_api.ex:505
#, elixir-format
msgid "Account not found"
msgstr "アカウントがりません"
msgstr "アカウントが見つかりません"
#: lib/pleroma/web/common_api/common_api.ex:339
#, elixir-format
@ -226,7 +226,7 @@ msgstr ""
#: lib/pleroma/web/common_api/utils.ex:414
#, elixir-format
msgid "Invalid password."
msgstr ""
msgstr "パスワードが間違っています。"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#, elixir-format
@ -236,7 +236,7 @@ msgstr ""
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
msgstr ""
msgstr "Kocaptchaが利用できません"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#, elixir-format
@ -259,12 +259,12 @@ msgstr ""
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format
msgid "Not found"
msgstr ""
msgstr "見つかりません"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
msgstr ""
msgstr "作成者は投票できません"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
@ -284,17 +284,17 @@ msgstr ""
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
msgstr ""
msgstr "公開範囲は「ダイレクト」でなければいけません"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
msgstr ""
msgstr "文字数制限を超過しています"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
msgstr ""
msgstr "認証が必要です。"
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
@ -304,7 +304,7 @@ msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
msgstr ""
msgstr "選択肢が多すぎます"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
@ -320,7 +320,7 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
msgstr ""
msgstr "あなたのアカウントは無効化されています"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
@ -331,23 +331,23 @@ msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "%{nickname}のinboxを%{as_nickname}として閲覧することはできません"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr ""
msgstr "%{nickname}のoutboxを%{as_nickname}として更新することはできません"
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
msgstr ""
msgstr "この会話はミュート済みです"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
msgstr ""
msgstr "エラー"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
@ -357,7 +357,7 @@ msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
msgstr ""
msgstr "見つかりません"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
@ -382,28 +382,28 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
msgstr ""
msgstr "認証に失敗しました: %{message}。"
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
msgstr ""
msgstr "ユーザーアカウントの設定に失敗しました。"
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
msgstr ""
msgstr "%{permissions}の権限が必要です。"
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
msgstr ""
msgstr "内部エラー"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
msgstr ""
msgstr "ユーザー名/パスワードが間違っています"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
@ -423,7 +423,7 @@ msgstr ""
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
msgstr ""
msgstr "不明なエラーです。詳細を確認し、再度試行してください。"
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
@ -449,17 +449,17 @@ msgstr ""
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
msgstr ""
msgstr "CAPTCHAエラー"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
msgid "Could not add reaction emoji"
msgstr ""
msgstr "リアクションを追加できません"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
msgstr ""
msgstr "リアクションを解除できません"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
@ -469,7 +469,7 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
msgstr ""
msgstr "リストが見つかりません"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
@ -480,7 +480,7 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
msgstr ""
msgstr "パスワードのリセットが必要です"
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
@ -522,7 +522,7 @@ msgstr ""
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
msgstr ""
msgstr "二段階認証が有効になっています。アクセストークンが必要です。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
@ -552,7 +552,7 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
msgstr ""
msgstr "このPleromaインスタンスではプッシュ通知が利用できません"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
@ -562,19 +562,19 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
msgstr ""
msgstr "タイムラインを閲覧するには認証が必要です"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
msgstr ""
msgstr "アクセスが拒否されました"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
msgstr ""
msgstr "このAPIを利用するには認証が必要です"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
msgstr ""
msgstr "ユーザーは管理者ではありません。"

View file

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

View file

@ -40,7 +40,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"
}
]
}

View file

@ -0,0 +1 @@
{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://cc.mkdir.uk/ap/note/e5d1d0a1-1ab3-4498-9949-588e3fdea286","attributedTo":"https://cc.mkdir.uk/ap/acct/hiira","inReplyTo":"","quoteUrl":"","content":"おはコンー","published":"2024-01-19T22:08:05Z","to":["https://www.w3.org/ns/activitystreams#Public"],"tag":null,"attachment":[],"object":null}

View 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"}}

View 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"}

View 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"}]}

View 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"]}

View file

@ -0,0 +1,60 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BookmarkFolderTest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.BookmarkFolder
describe "create/3" do
test "with valid params" do
user = insert(:user)
{:ok, folder} = BookmarkFolder.create(user.id, "Read later", "🕓")
assert folder.user_id == user.id
assert folder.name == "Read later"
assert folder.emoji == "🕓"
end
test "with invalid params" do
{:error, changeset} = BookmarkFolder.create(nil, "", "not an emoji")
refute changeset.valid?
assert changeset.errors == [
emoji: {"Invalid emoji", []},
user_id: {"can't be blank", [validation: :required]},
name: {"can't be blank", [validation: :required]}
]
end
end
test "update/3" do
user = insert(:user)
{:ok, folder} = BookmarkFolder.create(user.id, "Read ltaer")
{:ok, folder} = BookmarkFolder.update(folder.id, "Read later")
assert folder.name == "Read later"
end
test "for_user/1" do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = BookmarkFolder.create(user.id, "Folder 1")
{:ok, _} = BookmarkFolder.create(user.id, "Folder 2")
{:ok, _} = BookmarkFolder.create(other_user.id, "Folder 3")
folders = BookmarkFolder.for_user(user.id)
assert length(folders) == 2
end
test "belongs_to_user?/2" do
user = insert(:user)
other_user = insert(:user)
{:ok, folder} = BookmarkFolder.create(user.id, "Folder")
assert true == BookmarkFolder.belongs_to_user?(folder.id, user.id)
assert false == BookmarkFolder.belongs_to_user?(folder.id, other_user.id)
end
end

View file

@ -6,15 +6,17 @@ defmodule Pleroma.BookmarkTest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
alias Pleroma.Web.CommonAPI
describe "create/2" do
describe "create/3" do
test "with valid params" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"})
{:ok, bookmark} = Bookmark.create(user.id, activity.id)
assert bookmark.user_id == user.id
assert bookmark.activity_id == activity.id
assert bookmark.folder_id == nil
end
test "with invalid params" do
@ -26,6 +28,19 @@ defmodule Pleroma.BookmarkTest do
activity_id: {"can't be blank", [validation: :required]}
]
end
test "update existing bookmark folder" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"})
{:ok, bookmark} = Bookmark.create(user.id, activity.id)
assert bookmark.folder_id == nil
{:ok, bookmark_folder} = BookmarkFolder.create(user.id, "Read later")
{:ok, bookmark} = Bookmark.create(user.id, activity.id, bookmark_folder.id)
assert bookmark.folder_id == bookmark_folder.id
end
end
describe "destroy/2" do

View file

@ -268,17 +268,6 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end)
end
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
capture_log(fn ->
assert {:error, %WebSockex.RequestError{code: 401}} =
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
Process.sleep(30)
end)
end
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()

View file

@ -0,0 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MapsTest do
use Pleroma.DataCase, async: true
alias Pleroma.Maps
describe "filter_empty_values/1" do
assert %{"bar" => "b", "ray" => ["foo"], "objs" => %{"a" => "b"}} ==
Maps.filter_empty_values(%{
"foo" => nil,
"fooz" => "",
"bar" => "b",
"rei" => [],
"ray" => ["foo"],
"obj" => %{},
"objs" => %{"a" => "b"}
})
end
end

View file

@ -35,21 +35,6 @@ defmodule Pleroma.Search.DatabaseSearchTest do
assert [] = Search.search(nil, "wednesday")
end
test "using plainto_tsquery on postgres < 11" do
old_version = :persistent_term.get({Pleroma.Repo, :postgres_version})
:persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0)
on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end)
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
{:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
# plainto doesn't understand complex queries
assert [result] = Search.search(nil, "wednesday -dudes")
assert result.id == post.id
end
test "using websearch_to_tsquery" do
user = insert(:user)
{:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})

View file

@ -166,6 +166,7 @@ defmodule Pleroma.User.BackupTest do
test "it creates a zip archive with user data" do
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
%{ap_id: other_ap_id} = other_user = insert(:user)
{:ok, %{object: %{data: %{"id" => id1}}} = status1} =
CommonAPI.post(user, %{status: "status1"})
@ -182,6 +183,8 @@ defmodule Pleroma.User.BackupTest do
Bookmark.create(user.id, status2.id)
Bookmark.create(user.id, status3.id)
CommonAPI.follow(user, other_user)
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
assert {:ok, path} = Backup.export(backup, self())
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
@ -261,6 +264,16 @@ defmodule Pleroma.User.BackupTest do
"type" => "OrderedCollection"
} = Jason.decode!(json)
assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile)
assert %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "following.json",
"orderedItems" => [^other_ap_id],
"totalItems" => 1,
"type" => "OrderedCollection"
} = Jason.decode!(json)
:zip.zip_close(zipfile)
File.rm!(path)
end

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