Commit 50638525 authored by feld's avatar feld

Merge branch 'develop' into config/benchmark

parents df469b44 38ad4073
......@@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes`
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
......@@ -83,6 +84,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- ActivityPub: Configurable `type` field of the actors.
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
- Captcha: Support native provider
- Captcha: Enable by default
</details>
### Fixed
......@@ -91,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MRF: `Delete` activities being exempt from MRF policies
- OTP releases: Not being able to configure OAuth expired token cleanup interval
- OTP releases: Not being able to configure HTML sanitization policy
- Favorites timeline now ordered by favorite date instead of post date
<details>
<summary>API Changes</summary>
......
......@@ -66,9 +66,11 @@
jobs: scheduled_jobs
config :pleroma, Pleroma.Captcha,
enabled: false,
enabled: true,
seconds_valid: 60,
method: Pleroma.Captcha.Kocaptcha
method: Pleroma.Captcha.Native
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
config :pleroma, :hackney_pools,
federation: [
......@@ -84,8 +86,6 @@
timeout: 300_000
]
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
......
......@@ -68,7 +68,9 @@
queues: false,
prune: :disabled
config :pleroma, Pleroma.Scheduler, jobs: []
config :pleroma, Pleroma.Scheduler,
jobs: [],
global: false
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2,
......
......@@ -614,78 +614,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/respond`
## `POST /api/pleroma/admin/reports/:id/notes`
### Respond to a report
### Create report note
- Params:
- `id`
- `status`: required, the message
- `id`: required, report id
- `content`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
- On success: `204`, empty response
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
### Delete report note
- Params:
- `report_id`: required, report id
- `id`: required, note id
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
## `PUT /api/pleroma/admin/statuses/:id`
......
......@@ -379,13 +379,19 @@ For each pool, the options are:
## Captcha
### Pleroma.Captcha
* `enabled`: Whether the captcha should be shown on registration.
* `method`: The method/service to use for captcha.
* `seconds_valid`: The time in seconds for which the captcha is valid.
### Captcha providers
#### Pleroma.Captcha.Native
A built-in captcha provider. Enabled by default.
#### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer.
......
......@@ -12,6 +12,7 @@ defmodule Pleroma.Activity do
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.ThreadMute
alias Pleroma.User
......@@ -48,6 +49,8 @@ defmodule Pleroma.Activity do
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark)
# This is a fake relation, do not use outside of with_preloaded_report_notes
has_many(:report_notes, ReportNote)
has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
......@@ -114,6 +117,16 @@ def with_preloaded_bookmark(query, %User{} = user) do
def with_preloaded_bookmark(query, _), do: query
def with_preloaded_report_notes(query) do
from([a] in query,
left_join: r in ReportNote,
on: a.id == r.activity_id,
preload: [report_notes: r]
)
end
def with_preloaded_report_notes(query, _), do: query
def with_set_thread_muted_field(query, %User{} = user) do
from([a] in query,
left_join: tm in ThreadMute,
......
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service
@behaviour Service
@impl Service
def new do
case Captcha.get() do
{:timeout} ->
%{error: dgettext("errors", "Captcha timeout")}
{:ok, answer_data, img_binary} ->
%{
type: :native,
token: token(),
url: "data:image/png;base64," <> Base.encode64(img_binary),
answer_data: answer_data
}
end
end
@impl Service
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
defp token do
10
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
end
end
......@@ -128,17 +128,35 @@ def insert_log(%{
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_response",
action: "report_note",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_response",
"action" => "report_note",
"subject" => report_to_map(subject),
"text" => text,
"message" => ""
"text" => text
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_note_delete",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_note_delete",
"subject" => report_to_map(subject),
"text" => text
}
}
|> insert_log_entry_with_message()
......@@ -556,12 +574,24 @@ def get_log_entry_message(%ModerationLog{
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_response",
"action" => "report_note",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} added note '#{text}' to report ##{subject_id}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note_delete",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} responded with '#{text}' to report ##{subject_id}"
"@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
......
......@@ -23,6 +23,23 @@ defmodule Pleroma.Object do
timestamps()
end
def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
object_position = Map.get(query.aliases, :object, 0)
join(query, join_type, [{object, object_position}], a in Activity,
on:
fragment(
"COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
a.data,
object.data,
a.data,
^activity_type
),
as: :object_activity
)
end
def create(data) do
Object.change(%Object{}, %{data: data})
|> Repo.insert()
......
......@@ -13,60 +13,66 @@ defmodule Pleroma.Pagination do
alias Pleroma.Repo
@default_limit 20
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
def fetch_paginated(query, params, type \\ :keyset)
def page_keys, do: @page_keys
def fetch_paginated(query, %{"total" => true} = params, :keyset) do
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
total = Repo.aggregate(query, :count, :id)
%{
total: total,
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset)
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
}
end
def fetch_paginated(query, params, :keyset) do
def fetch_paginated(query, params, :keyset, table_binding) do
options = cast_params(params)
query
|> paginate(options, :keyset)
|> paginate(options, :keyset, table_binding)
|> Repo.all()
|> enforce_order(options)
end
def fetch_paginated(query, %{"total" => true} = params, :offset) do
total = Repo.aggregate(query, :count, :id)
def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
total =
query
|> Ecto.Query.exclude(:left_join)
|> Repo.aggregate(:count, :id)
%{
total: total,
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset)
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
}
end
def fetch_paginated(query, params, :offset) do
def fetch_paginated(query, params, :offset, table_binding) do
options = cast_params(params)
query
|> paginate(options, :offset)
|> paginate(options, :offset, table_binding)
|> Repo.all()
end
def paginate(query, options, method \\ :keyset)
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
def paginate(query, options, :keyset) do
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options)
|> restrict(:since_id, options)
|> restrict(:max_id, options)
|> restrict(:order, options)
|> restrict(:limit, options)
|> restrict(:min_id, options, table_binding)
|> restrict(:since_id, options, table_binding)
|> restrict(:max_id, options, table_binding)
|> restrict(:order, options, table_binding)
|> restrict(:limit, options, table_binding)
end
def paginate(query, options, :offset) do
def paginate(query, options, :offset, table_binding) do
query
|> restrict(:order, options)
|> restrict(:offset, options)
|> restrict(:limit, options)
|> restrict(:order, options, table_binding)
|> restrict(:offset, options, table_binding)
|> restrict(:limit, options, table_binding)
end
defp cast_params(params) do
......@@ -75,7 +81,8 @@ defp cast_params(params) do
since_id: :string,
max_id: :string,
offset: :integer,
limit: :integer
limit: :integer,
skip_order: :boolean
}
params =
......@@ -88,38 +95,48 @@ defp cast_params(params) do
changeset.changes
end
defp restrict(query, :min_id, %{min_id: min_id}) do
where(query, [q], q.id > ^min_id)
defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
end
defp restrict(query, :since_id, %{since_id: since_id}) do
where(query, [q], q.id > ^since_id)
defp restrict(query, :since_id, %{since_id: since_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id > ^since_id)
end
defp restrict(query, :max_id, %{max_id: max_id}) do
where(query, [q], q.id < ^max_id)
defp restrict(query, :max_id, %{max_id: max_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id)
end
defp restrict(query, :order, %{min_id: _}) do
order_by(query, [u], fragment("? asc nulls last", u.id))
defp restrict(query, :order, %{skip_order: true}, _), do: query
defp restrict(query, :order, %{min_id: _}, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? asc nulls last", u.id)
)
end
defp restrict(query, :order, _options) do
order_by(query, [u], fragment("? desc nulls last", u.id))
defp restrict(query, :order, _options, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? desc nulls last", u.id)
)
end
defp restrict(query, :offset, %{offset: offset}) do
defp restrict(query, :offset, %{offset: offset}, _table_binding) do
offset(query, ^offset)
end
defp restrict(query, :limit, options) do
defp restrict(query, :limit, options, _table_binding) do
limit = Map.get(options, :limit, @default_limit)
query
|> limit(^limit)
end
defp restrict(query, _, _), do: query
defp restrict(query, _, _, _), do: query
defp enforce_order(result, %{min_id: _}) do
result
......@@ -127,4 +144,10 @@ defp enforce_order(result, %{min_id: _}) do
end
defp enforce_order(result, _), do: result
defp table_position(%Ecto.Query{} = query, binding_name) do
Map.get(query.aliases, binding_name, 0)
end
defp table_position(_, _), do: 0
end
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReportNote do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.User
@type t :: %__MODULE__{}
schema "report_notes" do
field(:content, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
@spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) ::
{:ok, ReportNote.t()} | {:error, Changeset.t()}
def create(user_id, activity_id, content) do
attrs = %{
user_id: user_id,
activity_id: activity_id,
content: content
}
%ReportNote{}
|> cast(attrs, [:user_id, :activity_id, :content])
|> validate_required([:user_id, :activity_id, :content])
|> Repo.insert()
end
@spec destroy(FlakeId.Ecto.CompatType.t()) ::
{:ok, ReportNote.t()} | {:error, Changeset.t()}
def destroy(id) do
from(r in ReportNote, where: r.id == ^id)
|> Repo.one()
|> Repo.delete()
end
end
......@@ -1068,6 +1068,13 @@ defp maybe_preload_bookmarks(query, opts) do
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
query
|> Activity.with_preloaded_report_notes()
end
defp maybe_preload_report_notes(query, _), do: query
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
......@@ -1121,6 +1128,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
......@@ -1157,6 +1165,25 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|> maybe_update_cc(list_memberships, opts["user"])
end
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
@spec