Merge branch 'develop' into 'from/upstream-develop/tusooa/2892-backup-scope'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
tusooa 2022-09-05 15:00:19 +00:00
commit 9874b4c985
141 changed files with 6299 additions and 542 deletions

View File

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Allow users to remove their emails if instance does not need email to register - Allow users to remove their emails if instance does not need email to register
- Uploadfilter `Pleroma.Upload.Filter.Exiftool` has been renamed to `Pleroma.Upload.Filter.Exiftool.StripLocation` - Uploadfilter `Pleroma.Upload.Filter.Exiftool` has been renamed to `Pleroma.Upload.Filter.Exiftool.StripLocation`
- **Breaking**: `/api/v1/pleroma/backups` endpoints now requires `read:backups` scope instead of `read:accounts` - **Breaking**: `/api/v1/pleroma/backups` endpoints now requires `read:backups` scope instead of `read:accounts`
- Updated the recommended pleroma.vcl configuration for Varnish to target Varnish 7.0+
### Added ### Added
- `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object - `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object
@ -34,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field. - Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
- Uploadfilter `Pleroma.Upload.Filter.Exiftool.ReadDescription` returns description values to the FE so they can pre fill the image description field - Uploadfilter `Pleroma.Upload.Filter.Exiftool.ReadDescription` returns description values to the FE so they can pre fill the image description field
- Added move account API - Added move account API
- Enable remote users to interact with posts
### Fixed ### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies - Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
@ -49,6 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fixed crash when pinned_objects is nil - Fixed crash when pinned_objects is nil
- Fixed slow timelines when there are a lot of deactivated users - Fixed slow timelines when there are a lot of deactivated users
- Fixed account deletion API - Fixed account deletion API
- Fixed lowercase HTTP HEAD method in the Media Proxy Preview code
### Removed ### Removed

View File

@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev &&\
mkdir release &&\ mkdir release &&\
mix release --path release mix release --path release
FROM alpine:3.14 FROM alpine
ARG BUILD_DATE ARG BUILD_DATE
ARG VCS_REF ARG VCS_REF

View File

@ -673,6 +673,8 @@
config :pleroma, :populate_hashtags_table, fault_rate_allowance: 0.01 config :pleroma, :populate_hashtags_table, fault_rate_allowance: 0.01
config :pleroma, :delete_context_objects, fault_rate_allowance: 0.01
config :pleroma, :env, Mix.env() config :pleroma, :env, Mix.env()
config :http_signatures, config :http_signatures,
@ -741,7 +743,7 @@
"name" => "fedi-fe", "name" => "fedi-fe",
"git" => "https://git.pleroma.social/pleroma/fedi-fe", "git" => "https://git.pleroma.social/pleroma/fedi-fe",
"build_url" => "build_url" =>
"https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build", "https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build_release",
"ref" => "master", "ref" => "master",
"custom-http-headers" => [ "custom-http-headers" => [
{"service-worker-allowed", "/"} {"service-worker-allowed", "/"}
@ -761,6 +763,14 @@
"https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production", "https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production",
"ref" => "v1.0.0", "ref" => "v1.0.0",
"build_dir" => "static" "build_dir" => "static"
},
"glitch-lily" => %{
"name" => "glitch-lily",
"git" => "https://lily-is.land/infra/glitch-lily",
"build_url" =>
"https://lily-is.land/infra/glitch-lily/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "servant",
"build_dir" => "public"
} }
} }

View File

@ -495,6 +495,27 @@
} }
] ]
}, },
%{
group: :pleroma,
key: :delete_context_objects,
type: :group,
description: "`delete_context_objects` background migration settings",
children: [
%{
key: :fault_rate_allowance,
type: :float,
description:
"Max accepted rate of objects that failed in the migration. Any value from 0.0 which tolerates no errors to 1.0 which will enable the feature even if context object deletion failed for all records.",
suggestions: [0.01]
},
%{
key: :sleep_interval_ms,
type: :integer,
description:
"Sleep interval between each chunk of processed records in order to decrease the load on the system (defaults to 0 and should be keep default on most instances)."
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: :instance, key: :instance,

View File

@ -40,6 +40,10 @@ Has these additional fields under the `pleroma` object:
- `parent_visible`: If the parent of this post is visible to the user or not. - `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. - `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
- `content_type`: The content type of the status source.
## Scheduled statuses ## Scheduled statuses
Has these additional fields in `params`: Has these additional fields in `params`:

View File

@ -725,3 +725,42 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
* Authentication: required * Authentication: required
* Params: none * Params: none
* Response: HTTP 200 on success, 500 on error * Response: HTTP 200 on success, 500 on error
## `/api/v1/pleroma/settings/:app`
### Gets settings for some application
* Method `GET`
* Authentication: `read:accounts`
* Response: JSON. The settings for that application, or empty object if there is none.
* Example response:
```json
{
"some key": "some value"
}
```
### Updates settings for some application
* Method `PATCH`
* Authentication: `write:accounts`
* Request body: JSON object. The object will be merged recursively with old settings. If some field is set to null, it is removed.
* Example request:
```json
{
"some key": "some value",
"key to remove": null,
"nested field": {
"some key": "some value",
"key to remove": null
}
}
```
* Response: JSON. Updated (merged) settings for that application.
* Example response:
```json
{
"some key": "some value",
"nested field": {
"some key": "some value",
}
}
```

View File

@ -1,4 +1,5 @@
# Recommended varnishncsa logging format: '%h %l %u %t "%m %{X-Forwarded-Proto}i://%{Host}i%U%q %H" %s %b "%{Referer}i" "%{User-agent}i"' # Recommended varnishncsa logging format: '%h %l %u %t "%m %{X-Forwarded-Proto}i://%{Host}i%U%q %H" %s %b "%{Referer}i" "%{User-agent}i"'
# Please use Varnish 7.0+ for proper Range Requests / Chunked encoding support
vcl 4.1; vcl 4.1;
import std; import std;
@ -22,11 +23,6 @@ sub vcl_recv {
set req.http.X-Forwarded-Proto = "https"; set req.http.X-Forwarded-Proto = "https";
} }
# CHUNKED SUPPORT
if (req.http.Range ~ "bytes=") {
set req.http.x-range = req.http.Range;
}
# Pipe if WebSockets request is coming through # Pipe if WebSockets request is coming through
if (req.http.upgrade ~ "(?i)websocket") { if (req.http.upgrade ~ "(?i)websocket") {
return (pipe); return (pipe);
@ -35,9 +31,9 @@ sub vcl_recv {
# Allow purging of the cache # Allow purging of the cache
if (req.method == "PURGE") { if (req.method == "PURGE") {
if (!client.ip ~ purge) { if (!client.ip ~ purge) {
return(synth(405,"Not allowed.")); return (synth(405,"Not allowed."));
} }
return(purge); return (purge);
} }
} }
@ -53,17 +49,11 @@ sub vcl_backend_response {
return (retry); return (retry);
} }
# CHUNKED SUPPORT
if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) {
set beresp.ttl = 10m;
set beresp.http.CR = beresp.http.content-range;
}
# Bypass cache for large files # Bypass cache for large files
# 50000000 ~ 50MB # 50000000 ~ 50MB
if (std.integer(beresp.http.content-length, 0) > 50000000) { if (std.integer(beresp.http.content-length, 0) > 50000000) {
set beresp.uncacheable = true; set beresp.uncacheable = true;
return(deliver); return (deliver);
} }
# Don't cache objects that require authentication # Don't cache objects that require authentication
@ -94,7 +84,7 @@ sub vcl_synth {
if (resp.status == 750) { if (resp.status == 750) {
set resp.status = 301; set resp.status = 301;
set resp.http.Location = req.http.x-redir; set resp.http.Location = req.http.x-redir;
return(deliver); return (deliver);
} }
} }
@ -106,25 +96,12 @@ sub vcl_pipe {
} }
} }
sub vcl_hash {
# CHUNKED SUPPORT
if (req.http.x-range ~ "bytes=") {
hash_data(req.http.x-range);
unset req.http.Range;
}
}
sub vcl_backend_fetch { sub vcl_backend_fetch {
# Be more lenient for slow servers on the fediverse # Be more lenient for slow servers on the fediverse
if (bereq.url ~ "^/proxy/") { if (bereq.url ~ "^/proxy/") {
set bereq.first_byte_timeout = 300s; set bereq.first_byte_timeout = 300s;
} }
# CHUNKED SUPPORT
if (bereq.http.x-range) {
set bereq.http.Range = bereq.http.x-range;
}
if (bereq.retries == 0) { if (bereq.retries == 0) {
# Clean up the X-Varnish-Backend-503 flag that is used internally # Clean up the X-Varnish-Backend-503 flag that is used internally
# to mark broken backend responses that should be retried. # to mark broken backend responses that should be retried.
@ -143,14 +120,6 @@ sub vcl_backend_fetch {
} }
} }
sub vcl_deliver {
# CHUNKED SUPPORT
if (resp.http.CR) {
set resp.http.Content-Range = resp.http.CR;
unset resp.http.CR;
}
}
sub vcl_backend_error { sub vcl_backend_error {
# Retry broken backend responses. # Retry broken backend responses.
set bereq.http.X-Varnish-Backend-503 = "1"; set bereq.http.X-Varnish-Backend-503 = "1";

View File

@ -154,9 +154,8 @@ def run(["ensure_expiration"]) do
|> join(:inner, [a], o in Object, |> join(:inner, [a], o in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", "(?->>'id') = associated_object_id((?))",
o.data, o.data,
a.data,
a.data a.data
) )
) )

View File

@ -421,6 +421,38 @@ def run(["list"]) do
|> Stream.run() |> Stream.run()
end end
def run(["fix_follow_state", local_user, remote_user]) do
start_pleroma()
with {:local, %User{} = local} <- {:local, User.get_by_nickname(local_user)},
{:remote, %User{} = remote} <- {:remote, User.get_by_nickname(remote_user)},
{:follow_data, %{data: %{"state" => request_state}}} <-
{:follow_data, Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(local, remote)} do
calculated_state = User.following?(local, remote)
shell_info(
"Request state is #{request_state}, vs calculated state of following=#{calculated_state}"
)
if calculated_state == false && request_state == "accept" do
shell_info("Discrepancy found, fixing")
Pleroma.Web.CommonAPI.reject_follow_request(local, remote)
shell_info("Relationship fixed")
else
shell_info("No discrepancy found")
end
else
{:local, _} ->
shell_error("No local user #{local_user}")
{:remote, _} ->
shell_error("No remote user #{remote_user}")
{:follow_data, _} ->
shell_error("No follow data for #{local_user} and #{remote_user}")
end
end
defp set_moderator(user, value) do defp set_moderator(user, value) do
{:ok, user} = {:ok, user} =
user user

View File

@ -53,7 +53,7 @@ defmodule Pleroma.Activity do
# #
# ``` # ```
# |> join(:inner, [activity], o in Object, # |> join(:inner, [activity], o in Object,
# on: fragment("(?->>'id') = COALESCE((?)->'object'->> 'id', (?)->>'object')", # on: fragment("(?->>'id') = associated_object_id((?))",
# o.data, activity.data, activity.data)) # o.data, activity.data, activity.data))
# |> preload([activity, object], [object: object]) # |> preload([activity, object], [object: object])
# ``` # ```
@ -69,9 +69,8 @@ def with_joined_object(query, join_type \\ :inner) do
join(query, join_type, [activity], o in Object, join(query, join_type, [activity], o in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", "(?->>'id') = associated_object_id(?)",
o.data, o.data,
activity.data,
activity.data activity.data
), ),
as: :object as: :object

View File

@ -8,6 +8,40 @@ defmodule Pleroma.Activity.HTML do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex) @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
# We store a list of cache keys related to an activity in a
# separate cache, scrubber_management_cache. It has the same
# size as scrubber_cache (see application.ex). Every time we add
# a cache to scrubber_cache, we update scrubber_management_cache.
#
# The most recent write of a certain key in the management cache
# is the same as the most recent write of any record related to that
# key in the main cache.
# Assuming LRW ( https://hexdocs.pm/cachex/Cachex.Policy.LRW.html ),
# this means when the management cache is evicted by cachex, all
# related records in the main cache will also have been evicted.
defp get_cache_keys_for(activity_id) do
with {:ok, list} when is_list(list) <- @cachex.get(:scrubber_management_cache, activity_id) do
list
else
_ -> []
end
end
defp add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id)
unless additional_key in current do
@cachex.put(:scrubber_management_cache, activity_id, [additional_key | current])
end
end
def invalidate_cache_for(activity_id) do
keys = get_cache_keys_for(activity_id)
Enum.map(keys, &@cachex.del(:scrubber_cache, &1))
@cachex.del(:scrubber_management_cache, activity_id)
end
def get_cached_scrubbed_html_for_activity( def get_cached_scrubbed_html_for_activity(
content, content,
scrubbers, scrubbers,
@ -19,6 +53,8 @@ def get_cached_scrubbed_html_for_activity(
@cachex.fetch!(:scrubber_cache, key, fn _key -> @cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Object.normalize(activity, fetch: false) object = Object.normalize(activity, fetch: false)
add_cache_key_for(activity.id, key)
HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback) HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end) end)
end end

View File

@ -13,6 +13,14 @@ def get_activity_topics(activity) do
|> List.flatten() |> List.flatten()
end end
defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Delete"}}) do
["user", "user:pleroma_chat"]
end
defp generate_topics(%{data: %{"type" => "ChatMessage"}}, %{data: %{"type" => "Create"}}) do
[]
end
defp generate_topics(%{data: %{"type" => "Answer"}}, _) do defp generate_topics(%{data: %{"type" => "Answer"}}, _) do
[] []
end end
@ -21,7 +29,7 @@ defp generate_topics(object, activity) do
["user", "list"] ++ visibility_tags(object, activity) ["user", "list"] ++ visibility_tags(object, activity)
end end
defp visibility_tags(object, activity) do defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do
case Visibility.get_visibility(activity) do case Visibility.get_visibility(activity) do
"public" -> "public" ->
if activity.local do if activity.local do
@ -31,6 +39,10 @@ defp visibility_tags(object, activity) do
end end
|> item_creation_tags(object, activity) |> item_creation_tags(object, activity)
"local" ->
["public:local"]
|> item_creation_tags(object, activity)
"direct" -> "direct" ->
["direct"] ["direct"]
@ -39,6 +51,10 @@ defp visibility_tags(object, activity) do
end end
end end
defp visibility_tags(_object, _activity) do
[]
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++ tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
@ -63,7 +79,18 @@ defp remote_topics(_), do: []
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] defp attachment_topics(_object, %{local: true} = activity) do
case Visibility.get_visibility(activity) do
"public" ->
["public:media", "public:local:media"]
"local" ->
["public:local:media"]
_ ->
[]
end
end
defp attachment_topics(_object, %{actor: actor}) when is_binary(actor), defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]

View File

@ -52,8 +52,7 @@ def by_object_id(query, object_ids) when is_list(object_ids) do
activity in query, activity in query,
where: where:
fragment( fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", "associated_object_id((?)) = ANY(?)",
activity.data,
activity.data, activity.data,
^object_ids ^object_ids
) )
@ -64,8 +63,7 @@ def by_object_id(query, object_id) when is_binary(object_id) do
from(activity in query, from(activity in query,
where: where:
fragment( fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?", "associated_object_id((?)) = ?",
activity.data,
activity.data, activity.data,
^object_id ^object_id
) )

View File

@ -112,7 +112,17 @@ def start(_type, _args) do
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor] # If we have a lot of caches, default max_restarts can cause test
# resets to fail.
# Go for the default 3 unless we're in test
max_restarts =
if @mix_env == :test do
100
else
3
end
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
result = Supervisor.start_link(children, opts) result = Supervisor.start_link(children, opts)
set_postgres_server_version() set_postgres_server_version()
@ -189,6 +199,7 @@ defp cachex_children do
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500), build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000), build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500), build_cachex("scrubber", limit: 2500),
build_cachex("scrubber_management", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500), build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500), build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10), build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
@ -238,7 +249,8 @@ defp dont_run_in_test(_) do
defp background_migrators do defp background_migrators do
[ [
Pleroma.Migrators.HashtagsTableMigrator Pleroma.Migrators.HashtagsTableMigrator,
Pleroma.Migrators.ContextObjectsDeletionMigrator
] ]
end end

View File

@ -42,8 +42,45 @@ defp loop(state) do
def puts_activity(activity) do def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity}) status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})") IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HTML.strip_tags(status.content))
status.content
|> String.split("<br/>")
|> Enum.map(&HTML.strip_tags/1)
|> Enum.map(&HtmlEntities.decode/1)
|> Enum.map(&IO.puts/1)
end
def puts_notification(activity, user) do
notification =
Pleroma.Web.MastodonAPI.NotificationView.render("show.json", %{
notification: activity,
for: user
})
IO.puts(
"== (#{notification.type}) #{notification.status.id} by #{notification.account.display_name} (#{notification.account.acct})"
)
notification.status.content
|> String.split("<br/>")
|> Enum.map(&HTML.strip_tags/1)
|> Enum.map(&HtmlEntities.decode/1)
|> (fn x ->
case x do
[content] ->
"> " <> content
[head | _tail] ->
# "> " <> hd <> "..."
head
|> String.slice(1, 80)
|> (fn x -> "> " <> x <> "..." end).()
end
end).()
|> IO.puts()
IO.puts("") IO.puts("")
end end
@ -53,6 +90,11 @@ def handle_command(state, "help") do
IO.puts("home - Show the home timeline") IO.puts("home - Show the home timeline")
IO.puts("p <text> - Post the given text") IO.puts("p <text> - Post the given text")
IO.puts("r <id> <text> - Reply to the post with the given id") IO.puts("r <id> <text> - Reply to the post with the given id")
IO.puts("t <id> - Show a thread from the given id")
IO.puts("n - Show notifications")
IO.puts("n read - Mark all notifactions as read")
IO.puts("f <id> - Favourites the post with the given id")
IO.puts("R <id> - Repeat the post with the given id")
IO.puts("quit - Quit") IO.puts("quit - Quit")
state state
@ -73,11 +115,53 @@ def handle_command(%{user: user} = state, "r " <> text) do
state state
end end
def handle_command(%{user: user} = state, "t " <> activity_id) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
blocking_user: user,
user: user,
exclude_id: activity.id
})
case activities do
[] ->
activity_id
|> Activity.get_by_id()
|> puts_activity()
_ ->
activities
|> Enum.reverse()
|> Enum.each(&puts_activity/1)
end
else
_e -> IO.puts("Could not show this thread...")
end
state
end
def handle_command(%{user: user} = state, "n read") do
Pleroma.Notification.clear(user)
IO.puts("All notifications were marked as read")
state
end
def handle_command(%{user: user} = state, "n") do
user
|> Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(%{})
|> Enum.each(&puts_notification(&1, user))
state
end
def handle_command(%{user: user} = state, "p " <> text) do def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text) text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do with {:ok, activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!") IO.puts("Posted! ID: #{activity.id}")
else else
_e -> IO.puts("Could not post...") _e -> IO.puts("Could not post...")
end end
@ -85,6 +169,19 @@ def handle_command(%{user: user} = state, "p " <> text) do
state state
end end
def handle_command(%{user: user} = state, "f " <> id) do
id = String.trim(id)
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, _activity} <- CommonAPI.favorite(user, activity) do
IO.puts("Favourited!")
else
_e -> IO.puts("Could not Favourite...")
end
state
end
def handle_command(state, "home") do def handle_command(state, "home") do
user = state.user user = state.user
@ -123,7 +220,7 @@ defp wait_input(state, input) do
loop(%{state | counter: state.counter + 1}) loop(%{state | counter: state.counter + 1})
{:error, :interrupted} -> {:input, ^input, {:error, :interrupted}} ->
IO.puts("Caught Ctrl+C...") IO.puts("Caught Ctrl+C...")
loop(%{state | counter: state.counter + 1}) loop(%{state | counter: state.counter + 1})

View File

@ -28,6 +28,42 @@ defmodule Pleroma.Constants do
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
) )
const(status_updatable_fields,
do: [
"source",
"tag",
"updated",
"emoji",
"content",
"summary",
"sensitive",
"attachment",
"generator"
]
)
const(updatable_object_types,
do: [
"Note",
"Question",
"Audio",
"Video",
"Event",
"Article",
"Page"
]
)
const(actor_types,
do: [
"Application",
"Group",
"Organization",
"Person",
"Service"
]
)
# basic regex, just there to weed out potential mistakes # basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex, const(mime_regex,

View File

@ -42,4 +42,5 @@ def get_by_name(name) do
end end
def populate_hashtags_table, do: get_by_name("populate_hashtags_table") def populate_hashtags_table, do: get_by_name("populate_hashtags_table")
def delete_context_objects, do: get_by_name("delete_context_objects")
end end

View File

@ -1,13 +1,13 @@
# emoji-test.txt # emoji-test.txt
# Date: 2021-08-26, 17:22:23 GMT # Date: 2022-08-12, 20:24:39 GMT
# © 2021 Unicode®, Inc. # © 2022 Unicode®, Inc.
# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries.
# For terms of use, see http://www.unicode.org/terms_of_use.html # For terms of use, see https://www.unicode.org/terms_of_use.html
# #
# Emoji Keyboard/Display Test Data for UTS #51 # Emoji Keyboard/Display Test Data for UTS #51
# Version: 14.0 # Version: 15.0
# #
# For documentation and usage, see http://www.unicode.org/reports/tr51 # For documentation and usage, see https://www.unicode.org/reports/tr51
# #
# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed. # This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed.
# Format: code points; status # emoji name # Format: code points; status # emoji name
@ -92,6 +92,7 @@
1F62C ; fully-qualified # 😬 E1.0 grimacing face 1F62C ; fully-qualified # 😬 E1.0 grimacing face
1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling 1F62E 200D 1F4A8 ; fully-qualified # 😮‍💨 E13.1 face exhaling
1F925 ; fully-qualified # 🤥 E3.0 lying face 1F925 ; fully-qualified # 🤥 E3.0 lying face
1FAE8 ; fully-qualified # 🫨 E15.0 shaking face
# subgroup: face-sleepy # subgroup: face-sleepy
1F60C ; fully-qualified # 😌 E0.6 relieved face 1F60C ; fully-qualified # 😌 E0.6 relieved face
@ -155,7 +156,7 @@
# subgroup: face-negative # subgroup: face-negative
1F624 ; fully-qualified # 😤 E0.6 face with steam from nose 1F624 ; fully-qualified # 😤 E0.6 face with steam from nose
1F621 ; fully-qualified # 😡 E0.6 pouting face 1F621 ; fully-qualified # 😡 E0.6 enraged face
1F620 ; fully-qualified # 😠 E0.6 angry face 1F620 ; fully-qualified # 😠 E0.6 angry face
1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth 1F92C ; fully-qualified # 🤬 E5.0 face with symbols on mouth
1F608 ; fully-qualified # 😈 E1.0 smiling face with horns 1F608 ; fully-qualified # 😈 E1.0 smiling face with horns
@ -190,8 +191,7 @@
1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey 1F649 ; fully-qualified # 🙉 E0.6 hear-no-evil monkey
1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey 1F64A ; fully-qualified # 🙊 E0.6 speak-no-evil monkey
# subgroup: emotion # subgroup: heart
1F48B ; fully-qualified # 💋 E0.6 kiss mark
1F48C ; fully-qualified # 💌 E0.6 love letter 1F48C ; fully-qualified # 💌 E0.6 love letter
1F498 ; fully-qualified # 💘 E0.6 heart with arrow 1F498 ; fully-qualified # 💘 E0.6 heart with arrow
1F49D ; fully-qualified # 💝 E0.6 heart with ribbon 1F49D ; fully-qualified # 💝 E0.6 heart with ribbon
@ -210,14 +210,20 @@
2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart 2764 200D 1FA79 ; unqualified # ❤‍🩹 E13.1 mending heart
2764 FE0F ; fully-qualified # ❤️ E0.6 red heart 2764 FE0F ; fully-qualified # ❤️ E0.6 red heart
2764 ; unqualified # ❤ E0.6 red heart 2764 ; unqualified # ❤ E0.6 red heart
1FA77 ; fully-qualified # 🩷 E15.0 pink heart
1F9E1 ; fully-qualified # 🧡 E5.0 orange heart 1F9E1 ; fully-qualified # 🧡 E5.0 orange heart
1F49B ; fully-qualified # 💛 E0.6 yellow heart 1F49B ; fully-qualified # 💛 E0.6 yellow heart
1F49A ; fully-qualified # 💚 E0.6 green heart 1F49A ; fully-qualified # 💚 E0.6 green heart
1F499 ; fully-qualified # 💙 E0.6 blue heart 1F499 ; fully-qualified # 💙 E0.6 blue heart
1FA75 ; fully-qualified # 🩵 E15.0 light blue heart
1F49C ; fully-qualified # 💜 E0.6 purple heart 1F49C ; fully-qualified # 💜 E0.6 purple heart
1F90E ; fully-qualified # 🤎 E12.0 brown heart 1F90E ; fully-qualified # 🤎 E12.0 brown heart
1F5A4 ; fully-qualified # 🖤 E3.0 black heart 1F5A4 ; fully-qualified # 🖤 E3.0 black heart
1FA76 ; fully-qualified # 🩶 E15.0 grey heart
1F90D ; fully-qualified # 🤍 E12.0 white heart 1F90D ; fully-qualified # 🤍 E12.0 white heart
# subgroup: emotion
1F48B ; fully-qualified # 💋 E0.6 kiss mark
1F4AF ; fully-qualified # 💯 E0.6 hundred points 1F4AF ; fully-qualified # 💯 E0.6 hundred points
1F4A2 ; fully-qualified # 💢 E0.6 anger symbol 1F4A2 ; fully-qualified # 💢 E0.6 anger symbol
1F4A5 ; fully-qualified # 💥 E0.6 collision 1F4A5 ; fully-qualified # 💥 E0.6 collision
@ -226,21 +232,20 @@
1F4A8 ; fully-qualified # 💨 E0.6 dashing away 1F4A8 ; fully-qualified # 💨 E0.6 dashing away
1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole 1F573 FE0F ; fully-qualified # 🕳️ E0.7 hole
1F573 ; unqualified # 🕳 E0.7 hole 1F573 ; unqualified # 🕳 E0.7 hole
1F4A3 ; fully-qualified # 💣 E0.6 bomb
1F4AC ; fully-qualified # 💬 E0.6 speech balloon 1F4AC ; fully-qualified # 💬 E0.6 speech balloon
1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble 1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ E2.0 eye in speech bubble
1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble 1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ E2.0 eye in speech bubble
1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 E2.0 eye in speech bubble 1F441 FE0F 200D 1F5E8 ; minimally-qualified # 👁️‍🗨 E2.0 eye in speech bubble
1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble 1F441 200D 1F5E8 ; unqualified # 👁‍🗨 E2.0 eye in speech bubble
1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble 1F5E8 FE0F ; fully-qualified # 🗨️ E2.0 left speech bubble
1F5E8 ; unqualified # 🗨 E2.0 left speech bubble 1F5E8 ; unqualified # 🗨 E2.0 left speech bubble
1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble 1F5EF FE0F ; fully-qualified # 🗯️ E0.7 right anger bubble
1F5EF ; unqualified # 🗯 E0.7 right anger bubble 1F5EF ; unqualified # 🗯 E0.7 right anger bubble
1F4AD ; fully-qualified # 💭 E1.0 thought balloon 1F4AD ; fully-qualified # 💭 E1.0 thought balloon
1F4A4 ; fully-qualified # 💤 E0.6 zzz 1F4A4 ; fully-qualified # 💤 E0.6 ZZZ
# Smileys & Emotion subtotal: 177 # Smileys & Emotion subtotal: 180
# Smileys & Emotion subtotal: 177 w/o modifiers # Smileys & Emotion subtotal: 180 w/o modifiers
# group: People & Body # group: People & Body
@ -300,6 +305,18 @@
1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone 1FAF4 1F3FD ; fully-qualified # 🫴🏽 E14.0 palm up hand: medium skin tone
1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone 1FAF4 1F3FE ; fully-qualified # 🫴🏾 E14.0 palm up hand: medium-dark skin tone
1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone 1FAF4 1F3FF ; fully-qualified # 🫴🏿 E14.0 palm up hand: dark skin tone
1FAF7 ; fully-qualified # 🫷 E15.0 leftwards pushing hand
1FAF7 1F3FB ; fully-qualified # 🫷🏻 E15.0 leftwards pushing hand: light skin tone
1FAF7 1F3FC ; fully-qualified # 🫷🏼 E15.0 leftwards pushing hand: medium-light skin tone
1FAF7 1F3FD ; fully-qualified # 🫷🏽 E15.0 leftwards pushing hand: medium skin tone
1FAF7 1F3FE ; fully-qualified # 🫷🏾 E15.0 leftwards pushing hand: medium-dark skin tone
1FAF7 1F3FF ; fully-qualified # 🫷🏿 E15.0 leftwards pushing hand: dark skin tone
1FAF8 ; fully-qualified # 🫸 E15.0 rightwards pushing hand
1FAF8 1F3FB ; fully-qualified # 🫸🏻 E15.0 rightwards pushing hand: light skin tone
1FAF8 1F3FC ; fully-qualified # 🫸🏼 E15.0 rightwards pushing hand: medium-light skin tone
1FAF8 1F3FD ; fully-qualified # 🫸🏽 E15.0 rightwards pushing hand: medium skin tone
1FAF8 1F3FE ; fully-qualified # 🫸🏾 E15.0 rightwards pushing hand: medium-dark skin tone
1FAF8 1F3FF ; fully-qualified # 🫸🏿 E15.0 rightwards pushing hand: dark skin tone
# subgroup: hand-fingers-partial # subgroup: hand-fingers-partial
1F44C ; fully-qualified # 👌 E0.6 OK hand 1F44C ; fully-qualified # 👌 E0.6 OK hand
@ -473,11 +490,11 @@
1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone 1F932 1F3FE ; fully-qualified # 🤲🏾 E5.0 palms up together: medium-dark skin tone
1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone 1F932 1F3FF ; fully-qualified # 🤲🏿 E5.0 palms up together: dark skin tone
1F91D ; fully-qualified # 🤝 E3.0 handshake 1F91D ; fully-qualified # 🤝 E3.0 handshake
1F91D 1F3FB ; fully-qualified # 🤝🏻 E3.0 handshake: light skin tone 1F91D 1F3FB ; fully-qualified # 🤝🏻 E14.0 handshake: light skin tone
1F91D 1F3FC ; fully-qualified # 🤝🏼 E3.0 handshake: medium-light skin tone 1F91D 1F3FC ; fully-qualified # 🤝🏼 E14.0 handshake: medium-light skin tone
1F91D 1F3FD ; fully-qualified # 🤝🏽 E3.0 handshake: medium skin tone 1F91D 1F3FD ; fully-qualified # 🤝🏽 E14.0 handshake: medium skin tone
1F91D 1F3FE ; fully-qualified # 🤝🏾 E3.0 handshake: medium-dark skin tone 1F91D 1F3FE ; fully-qualified # 🤝🏾 E14.0 handshake: medium-dark skin tone
1F91D 1F3FF ; fully-qualified # 🤝🏿 E3.0 handshake: dark skin tone 1F91D 1F3FF ; fully-qualified # 🤝🏿 E14.0 handshake: dark skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone 1FAF1 1F3FB 200D 1FAF2 1F3FC ; fully-qualified # 🫱🏻‍🫲🏼 E14.0 handshake: light skin tone, medium-light skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone 1FAF1 1F3FB 200D 1FAF2 1F3FD ; fully-qualified # 🫱🏻‍🫲🏽 E14.0 handshake: light skin tone, medium skin tone
1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone 1FAF1 1F3FB 200D 1FAF2 1F3FE ; fully-qualified # 🫱🏻‍🫲🏾 E14.0 handshake: light skin tone, medium-dark skin tone
@ -1455,7 +1472,7 @@
1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone 1F575 1F3FF ; fully-qualified # 🕵🏿 E2.0 detective: dark skin tone
1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective 1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ E4.0 man detective
1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective 1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ E4.0 man detective
1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ E4.0 man detective 1F575 FE0F 200D 2642 ; minimally-qualified # 🕵️‍♂ E4.0 man detective
1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective 1F575 200D 2642 ; unqualified # 🕵‍♂ E4.0 man detective
1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone 1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ E4.0 man detective: light skin tone
1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone 1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ E4.0 man detective: light skin tone
@ -1469,7 +1486,7 @@
1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone 1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ E4.0 man detective: dark skin tone
1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective 1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ E4.0 woman detective
1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective 1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ E4.0 woman detective
1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ E4.0 woman detective 1F575 FE0F 200D 2640 ; minimally-qualified # 🕵️‍♀ E4.0 woman detective
1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective 1F575 200D 2640 ; unqualified # 🕵‍♀ E4.0 woman detective
1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone 1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ E4.0 woman detective: light skin tone
1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone 1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ E4.0 woman detective: light skin tone
@ -2302,7 +2319,7 @@
1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone 1F3CC 1F3FF ; fully-qualified # 🏌🏿 E4.0 person golfing: dark skin tone
1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing 1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ E4.0 man golfing
1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing 1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ E4.0 man golfing
1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ E4.0 man golfing 1F3CC FE0F 200D 2642 ; minimally-qualified # 🏌️‍♂ E4.0 man golfing
1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing 1F3CC 200D 2642 ; unqualified # 🏌‍♂ E4.0 man golfing
1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone 1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ E4.0 man golfing: light skin tone
1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone 1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ E4.0 man golfing: light skin tone
@ -2316,7 +2333,7 @@
1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone 1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ E4.0 man golfing: dark skin tone
1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing 1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ E4.0 woman golfing
1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing 1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ E4.0 woman golfing
1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ E4.0 woman golfing 1F3CC FE0F 200D 2640 ; minimally-qualified # 🏌️‍♀ E4.0 woman golfing
1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing 1F3CC 200D 2640 ; unqualified # 🏌‍♀ E4.0 woman golfing
1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone 1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ E4.0 woman golfing: light skin tone
1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone 1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ E4.0 woman golfing: light skin tone
@ -2427,7 +2444,7 @@
26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone 26F9 1F3FF ; fully-qualified # ⛹🏿 E2.0 person bouncing ball: dark skin tone
26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball 26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ E4.0 man bouncing ball
26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball 26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ E4.0 man bouncing ball
26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ E4.0 man bouncing ball 26F9 FE0F 200D 2642 ; minimally-qualified # ⛹️‍♂ E4.0 man bouncing ball
26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball 26F9 200D 2642 ; unqualified # ⛹‍♂ E4.0 man bouncing ball
26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone 26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ E4.0 man bouncing ball: light skin tone
26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone 26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ E4.0 man bouncing ball: light skin tone
@ -2441,7 +2458,7 @@
26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone 26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ E4.0 man bouncing ball: dark skin tone
26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball 26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ E4.0 woman bouncing ball
26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball 26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ E4.0 woman bouncing ball
26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ E4.0 woman bouncing ball 26F9 FE0F 200D 2640 ; minimally-qualified # ⛹️‍♀ E4.0 woman bouncing ball
26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball 26F9 200D 2640 ; unqualified # ⛹‍♀ E4.0 woman bouncing ball
26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone 26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ E4.0 woman bouncing ball: light skin tone
26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone 26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ E4.0 woman bouncing ball: light skin tone
@ -2462,7 +2479,7 @@
1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone 1F3CB 1F3FF ; fully-qualified # 🏋🏿 E2.0 person lifting weights: dark skin tone
1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights 1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ E4.0 man lifting weights
1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights 1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ E4.0 man lifting weights
1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ E4.0 man lifting weights 1F3CB FE0F 200D 2642 ; minimally-qualified # 🏋️‍♂ E4.0 man lifting weights
1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights 1F3CB 200D 2642 ; unqualified # 🏋‍♂ E4.0 man lifting weights
1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone 1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ E4.0 man lifting weights: light skin tone
1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone 1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ E4.0 man lifting weights: light skin tone
@ -2476,7 +2493,7 @@
1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone 1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ E4.0 man lifting weights: dark skin tone
1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights 1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ E4.0 woman lifting weights
1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights 1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ E4.0 woman lifting weights
1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ E4.0 woman lifting weights 1F3CB FE0F 200D 2640 ; minimally-qualified # 🏋️‍♀ E4.0 woman lifting weights
1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights 1F3CB 200D 2640 ; unqualified # 🏋‍♀ E4.0 woman lifting weights
1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone 1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ E4.0 woman lifting weights: light skin tone
1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone 1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ E4.0 woman lifting weights: light skin tone
@ -3262,8 +3279,8 @@
1FAC2 ; fully-qualified # 🫂 E13.0 people hugging 1FAC2 ; fully-qualified # 🫂 E13.0 people hugging
1F463 ; fully-qualified # 👣 E0.6 footprints 1F463 ; fully-qualified # 👣 E0.6 footprints
# People & Body subtotal: 2986 # People & Body subtotal: 2998
# People & Body subtotal: 506 w/o modifiers # People & Body subtotal: 508 w/o modifiers
# group: Component # group: Component
@ -3306,6 +3323,8 @@
1F405 ; fully-qualified # 🐅 E1.0 tiger 1F405 ; fully-qualified # 🐅 E1.0 tiger
1F406 ; fully-qualified # 🐆 E1.0 leopard 1F406 ; fully-qualified # 🐆 E1.0 leopard
1F434 ; fully-qualified # 🐴 E0.6 horse face 1F434 ; fully-qualified # 🐴 E0.6 horse face
1FACE ; fully-qualified # 🫎 E15.0 moose
1FACF ; fully-qualified # 🫏 E15.0 donkey
1F40E ; fully-qualified # 🐎 E0.6 horse 1F40E ; fully-qualified # 🐎 E0.6 horse
1F984 ; fully-qualified # 🦄 E1.0 unicorn 1F984 ; fully-qualified # 🦄 E1.0 unicorn
1F993 ; fully-qualified # 🦓 E5.0 zebra 1F993 ; fully-qualified # 🦓 E5.0 zebra
@ -3373,6 +3392,9 @@
1F9A9 ; fully-qualified # 🦩 E12.0 flamingo 1F9A9 ; fully-qualified # 🦩 E12.0 flamingo
1F99A ; fully-qualified # 🦚 E11.0 peacock 1F99A ; fully-qualified # 🦚 E11.0 peacock
1F99C ; fully-qualified # 🦜 E11.0 parrot 1F99C ; fully-qualified # 🦜 E11.0 parrot
1FABD ; fully-qualified # 🪽 E15.0 wing
1F426 200D 2B1B ; fully-qualified # 🐦‍⬛ E15.0 black bird
1FABF ; fully-qualified # 🪿 E15.0 goose
# subgroup: animal-amphibian # subgroup: animal-amphibian
1F438 ; fully-qualified # 🐸 E0.6 frog 1F438 ; fully-qualified # 🐸 E0.6 frog
@ -3399,6 +3421,7 @@
1F419 ; fully-qualified # 🐙 E0.6 octopus 1F419 ; fully-qualified # 🐙 E0.6 octopus
1F41A ; fully-qualified # 🐚 E0.6 spiral shell 1F41A ; fully-qualified # 🐚 E0.6 spiral shell
1FAB8 ; fully-qualified # 🪸 E14.0 coral 1FAB8 ; fully-qualified # 🪸 E14.0 coral
1FABC ; fully-qualified # 🪼 E15.0 jellyfish
# subgroup: animal-bug # subgroup: animal-bug
1F40C ; fully-qualified # 🐌 E0.6 snail 1F40C ; fully-qualified # 🐌 E0.6 snail
@ -3433,6 +3456,7 @@
1F33B ; fully-qualified # 🌻 E0.6 sunflower 1F33B ; fully-qualified # 🌻 E0.6 sunflower
1F33C ; fully-qualified # 🌼 E0.6 blossom 1F33C ; fully-qualified # 🌼 E0.6 blossom
1F337 ; fully-qualified # 🌷 E0.6 tulip 1F337 ; fully-qualified # 🌷 E0.6 tulip
1FABB ; fully-qualified # 🪻 E15.0 hyacinth
# subgroup: plant-other # subgroup: plant-other
1F331 ; fully-qualified # 🌱 E0.6 seedling 1F331 ; fully-qualified # 🌱 E0.6 seedling
@ -3451,9 +3475,10 @@
1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind 1F343 ; fully-qualified # 🍃 E0.6 leaf fluttering in wind
1FAB9 ; fully-qualified # 🪹 E14.0 empty nest 1FAB9 ; fully-qualified # 🪹 E14.0 empty nest
1FABA ; fully-qualified # 🪺 E14.0 nest with eggs 1FABA ; fully-qualified # 🪺 E14.0 nest with eggs
1F344 ; fully-qualified # 🍄 E0.6 mushroom
# Animals & Nature subtotal: 151 # Animals & Nature subtotal: 159
# Animals & Nature subtotal: 151 w/o modifiers # Animals & Nature subtotal: 159 w/o modifiers
# group: Food & Drink # group: Food & Drink
@ -3492,10 +3517,11 @@
1F966 ; fully-qualified # 🥦 E5.0 broccoli 1F966 ; fully-qualified # 🥦 E5.0 broccoli
1F9C4 ; fully-qualified # 🧄 E12.0 garlic 1F9C4 ; fully-qualified # 🧄 E12.0 garlic
1F9C5 ; fully-qualified # 🧅 E12.0 onion 1F9C5 ; fully-qualified # 🧅 E12.0 onion
1F344 ; fully-qualified # 🍄 E0.6 mushroom
1F95C ; fully-qualified # 🥜 E3.0 peanuts 1F95C ; fully-qualified # 🥜 E3.0 peanuts
1FAD8 ; fully-qualified # 🫘 E14.0 beans 1FAD8 ; fully-qualified # 🫘 E14.0 beans
1F330 ; fully-qualified # 🌰 E0.6 chestnut 1F330 ; fully-qualified # 🌰 E0.6 chestnut
1FADA ; fully-qualified # 🫚 E15.0 ginger root
1FADB ; fully-qualified # 🫛 E15.0 pea pod
# subgroup: food-prepared # subgroup: food-prepared
1F35E ; fully-qualified # 🍞 E0.6 bread 1F35E ; fully-qualified # 🍞 E0.6 bread
@ -3607,8 +3633,8 @@
1FAD9 ; fully-qualified # 🫙 E14.0 jar 1FAD9 ; fully-qualified # 🫙 E14.0 jar
1F3FA ; fully-qualified # 🏺 E1.0 amphora 1F3FA ; fully-qualified # 🏺 E1.0 amphora
# Food & Drink subtotal: 134 # Food & Drink subtotal: 135
# Food & Drink subtotal: 134 w/o modifiers # Food & Drink subtotal: 135 w/o modifiers
# group: Travel & Places # group: Travel & Places
@ -3974,11 +4000,10 @@
1F3AF ; fully-qualified # 🎯 E0.6 bullseye 1F3AF ; fully-qualified # 🎯 E0.6 bullseye
1FA80 ; fully-qualified # 🪀 E12.0 yo-yo 1FA80 ; fully-qualified # 🪀 E12.0 yo-yo
1FA81 ; fully-qualified # 🪁 E12.0 kite 1FA81 ; fully-qualified # 🪁 E12.0 kite
1F52B ; fully-qualified # 🔫 E0.6 water pistol
1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball 1F3B1 ; fully-qualified # 🎱 E0.6 pool 8 ball
1F52E ; fully-qualified # 🔮 E0.6 crystal ball 1F52E ; fully-qualified # 🔮 E0.6 crystal ball
1FA84 ; fully-qualified # 🪄 E13.0 magic wand 1FA84 ; fully-qualified # 🪄 E13.0 magic wand
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F3AE ; fully-qualified # 🎮 E0.6 video game 1F3AE ; fully-qualified # 🎮 E0.6 video game
1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick 1F579 FE0F ; fully-qualified # 🕹️ E0.7 joystick
1F579 ; unqualified # 🕹 E0.7 joystick 1F579 ; unqualified # 🕹 E0.7 joystick
@ -4013,8 +4038,8 @@
1F9F6 ; fully-qualified # 🧶 E11.0 yarn 1F9F6 ; fully-qualified # 🧶 E11.0 yarn
1FAA2 ; fully-qualified # 🪢 E13.0 knot 1FAA2 ; fully-qualified # 🪢 E13.0 knot
# Activities subtotal: 97 # Activities subtotal: 96
# Activities subtotal: 97 w/o modifiers # Activities subtotal: 96 w/o modifiers
# group: Objects # group: Objects
@ -4040,6 +4065,7 @@
1FA73 ; fully-qualified # 🩳 E12.0 shorts 1FA73 ; fully-qualified # 🩳 E12.0 shorts
1F459 ; fully-qualified # 👙 E0.6 bikini 1F459 ; fully-qualified # 👙 E0.6 bikini
1F45A ; fully-qualified # 👚 E0.6 womans clothes 1F45A ; fully-qualified # 👚 E0.6 womans clothes
1FAAD ; fully-qualified # 🪭 E15.0 folding hand fan
1F45B ; fully-qualified # 👛 E0.6 purse 1F45B ; fully-qualified # 👛 E0.6 purse
1F45C ; fully-qualified # 👜 E0.6 handbag 1F45C ; fully-qualified # 👜 E0.6 handbag
1F45D ; fully-qualified # 👝 E0.6 clutch bag 1F45D ; fully-qualified # 👝 E0.6 clutch bag
@ -4055,6 +4081,7 @@
1F461 ; fully-qualified # 👡 E0.6 womans sandal 1F461 ; fully-qualified # 👡 E0.6 womans sandal
1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes 1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
1F462 ; fully-qualified # 👢 E0.6 womans boot 1F462 ; fully-qualified # 👢 E0.6 womans boot
1FAAE ; fully-qualified # 🪮 E15.0 hair pick
1F451 ; fully-qualified # 👑 E0.6 crown 1F451 ; fully-qualified # 👑 E0.6 crown
1F452 ; fully-qualified # 👒 E0.6 womans hat 1F452 ; fully-qualified # 👒 E0.6 womans hat
1F3A9 ; fully-qualified # 🎩 E0.6 top hat 1F3A9 ; fully-qualified # 🎩 E0.6 top hat
@ -4103,6 +4130,8 @@
1FA95 ; fully-qualified # 🪕 E12.0 banjo 1FA95 ; fully-qualified # 🪕 E12.0 banjo
1F941 ; fully-qualified # 🥁 E3.0 drum 1F941 ; fully-qualified # 🥁 E3.0 drum
1FA98 ; fully-qualified # 🪘 E13.0 long drum 1FA98 ; fully-qualified # 🪘 E13.0 long drum
1FA87 ; fully-qualified # 🪇 E15.0 maracas
1FA88 ; fully-qualified # 🪈 E15.0 flute
# subgroup: phone # subgroup: phone
1F4F1 ; fully-qualified # 📱 E0.6 mobile phone 1F4F1 ; fully-qualified # 📱 E0.6 mobile phone
@ -4275,7 +4304,7 @@
1F5E1 ; unqualified # 🗡 E0.7 dagger 1F5E1 ; unqualified # 🗡 E0.7 dagger
2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords 2694 FE0F ; fully-qualified # ⚔️ E1.0 crossed swords
2694 ; unqualified # ⚔ E1.0 crossed swords 2694 ; unqualified # ⚔ E1.0 crossed swords
1F52B ; fully-qualified # 🔫 E0.6 water pistol 1F4A3 ; fully-qualified # 💣 E0.6 bomb
1FA83 ; fully-qualified # 🪃 E13.0 boomerang 1FA83 ; fully-qualified # 🪃 E13.0 boomerang
1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow 1F3F9 ; fully-qualified # 🏹 E1.0 bow and arrow
1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield 1F6E1 FE0F ; fully-qualified # 🛡️ E0.7 shield
@ -4354,12 +4383,14 @@
1FAA6 ; fully-qualified # 🪦 E13.0 headstone 1FAA6 ; fully-qualified # 🪦 E13.0 headstone
26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn 26B1 FE0F ; fully-qualified # ⚱️ E1.0 funeral urn
26B1 ; unqualified # ⚱ E1.0 funeral urn 26B1 ; unqualified # ⚱ E1.0 funeral urn
1F9FF ; fully-qualified # 🧿 E11.0 nazar amulet
1FAAC ; fully-qualified # 🪬 E14.0 hamsa
1F5FF ; fully-qualified # 🗿 E0.6 moai 1F5FF ; fully-qualified # 🗿 E0.6 moai
1FAA7 ; fully-qualified # 🪧 E13.0 placard 1FAA7 ; fully-qualified # 🪧 E13.0 placard
1FAAA ; fully-qualified # 🪪 E14.0 identification card 1FAAA ; fully-qualified # 🪪 E14.0 identification card
# Objects subtotal: 304 # Objects subtotal: 310
# Objects subtotal: 304 w/o modifiers # Objects subtotal: 310 w/o modifiers
# group: Symbols # group: Symbols
@ -4455,6 +4486,7 @@
262E ; unqualified # ☮ E1.0 peace symbol 262E ; unqualified # ☮ E1.0 peace symbol
1F54E ; fully-qualified # 🕎 E1.0 menorah 1F54E ; fully-qualified # 🕎 E1.0 menorah
1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star 1F52F ; fully-qualified # 🔯 E0.6 dotted six-pointed star
1FAAF ; fully-qualified # 🪯 E15.0 khanda
# subgroup: zodiac # subgroup: zodiac
2648 ; fully-qualified # ♈ E0.6 Aries 2648 ; fully-qualified # ♈ E0.6 Aries
@ -4503,6 +4535,7 @@
1F505 ; fully-qualified # 🔅 E1.0 dim button 1F505 ; fully-qualified # 🔅 E1.0 dim button
1F506 ; fully-qualified # 🔆 E1.0 bright button 1F506 ; fully-qualified # 🔆 E1.0 bright button
1F4F6 ; fully-qualified # 📶 E0.6 antenna bars 1F4F6 ; fully-qualified # 📶 E0.6 antenna bars
1F6DC ; fully-qualified # 🛜 E15.0 wireless
1F4F3 ; fully-qualified # 📳 E0.6 vibration mode 1F4F3 ; fully-qualified # 📳 E0.6 vibration mode
1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off 1F4F4 ; fully-qualified # 📴 E0.6 mobile phone off
@ -4693,8 +4726,8 @@
1F533 ; fully-qualified # 🔳 E0.6 white square button 1F533 ; fully-qualified # 🔳 E0.6 white square button
1F532 ; fully-qualified # 🔲 E0.6 black square button 1F532 ; fully-qualified # 🔲 E0.6 black square button
# Symbols subtotal: 302 # Symbols subtotal: 304
# Symbols subtotal: 302 w/o modifiers # Symbols subtotal: 304 w/o modifiers
# group: Flags # group: Flags
@ -4709,7 +4742,7 @@
1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag 1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 E4.0 rainbow flag
1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag 1F3F3 FE0F 200D 26A7 FE0F ; fully-qualified # 🏳️‍⚧️ E13.0 transgender flag
1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag 1F3F3 200D 26A7 FE0F ; unqualified # 🏳‍⚧️ E13.0 transgender flag
1F3F3 FE0F 200D 26A7 ; unqualified # 🏳️‍⚧ E13.0 transgender flag 1F3F3 FE0F 200D 26A7 ; minimally-qualified # 🏳️‍⚧ E13.0 transgender flag
1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag 1F3F3 200D 26A7 ; unqualified # 🏳‍⚧ E13.0 transgender flag
1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag 1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ E11.0 pirate flag
1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag 1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ E11.0 pirate flag
@ -4983,9 +5016,9 @@
# Flags subtotal: 275 w/o modifiers # Flags subtotal: 275 w/o modifiers
# Status Counts # Status Counts
# fully-qualified : 3624 # fully-qualified : 3655
# minimally-qualified : 817 # minimally-qualified : 827
# unqualified : 252 # unqualified : 242
# component : 9 # component : 9
#EOF #EOF

View File

@ -0,0 +1,139 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Migrators.ContextObjectsDeletionMigrator do
defmodule State do
use Pleroma.Migrators.Support.BaseMigratorState
@impl Pleroma.Migrators.Support.BaseMigratorState
defdelegate data_migration(), to: Pleroma.DataMigration, as: :delete_context_objects
end
use Pleroma.Migrators.Support.BaseMigrator
alias Pleroma.Migrators.Support.BaseMigrator
alias Pleroma.Object
@doc "This migration removes objects created exclusively for contexts, containing only an `id` field."
@impl BaseMigrator
def feature_config_path, do: [:features, :delete_context_objects]
@impl BaseMigrator
def fault_rate_allowance, do: Config.get([:delete_context_objects, :fault_rate_allowance], 0)
@impl BaseMigrator
def perform do
data_migration_id = data_migration_id()
max_processed_id = get_stat(:max_processed_id, 0)
Logger.info("Deleting context objects from `objects` (from oid: #{max_processed_id})...")
query()
|> where([object], object.id > ^max_processed_id)
|> Repo.chunk_stream(100, :batches, timeout: :infinity)
|> Stream.each(fn objects ->
object_ids = Enum.map(objects, & &1.id)
results = Enum.map(object_ids, &delete_context_object(&1))
failed_ids =
results
|> Enum.filter(&(elem(&1, 0) == :error))
|> Enum.map(&elem(&1, 1))
chunk_affected_count =
results
|> Enum.filter(&(elem(&1, 0) == :ok))
|> length()
for failed_id <- failed_ids do
_ =
Repo.query(
"INSERT INTO data_migration_failed_ids(data_migration_id, record_id) " <>
"VALUES ($1, $2) ON CONFLICT DO NOTHING;",
[data_migration_id, failed_id]
)
end
_ =
Repo.query(
"DELETE FROM data_migration_failed_ids " <>
"WHERE data_migration_id = $1 AND record_id = ANY($2)",
[data_migration_id, object_ids -- failed_ids]
)
max_object_id = Enum.at(object_ids, -1)
put_stat(:max_processed_id, max_object_id)
increment_stat(:iteration_processed_count, length(object_ids))
increment_stat(:processed_count, length(object_ids))
increment_stat(:failed_count, length(failed_ids))
increment_stat(:affected_count, chunk_affected_count)
put_stat(:records_per_second, records_per_second())
persist_state()
# A quick and dirty approach to controlling the load this background migration imposes
sleep_interval = Config.get([:delete_context_objects, :sleep_interval_ms], 0)
Process.sleep(sleep_interval)
end)
|> Stream.run()
end
@impl BaseMigrator
def query do
# Context objects have no activity type, and only one field, `id`.
# Only those context objects are without types.
from(
object in Object,
where: fragment("(?)->'type' IS NULL", object.data),
select: %{
id: object.id
}
)
end
@spec delete_context_object(integer()) :: {:ok | :error, integer()}
defp delete_context_object(id) do
result =
%Object{id: id}
|> Repo.delete()
|> elem(0)
{result, id}
end
@impl BaseMigrator
def retry_failed do
data_migration_id = data_migration_id()
failed_objects_query()
|> Repo.chunk_stream(100, :one)
|> Stream.each(fn object ->
with {res, _} when res != :error <- delete_context_object(object.id) do
_ =
Repo.query(
"DELETE FROM data_migration_failed_ids " <>
"WHERE data_migration_id = $1 AND record_id = $2",
[data_migration_id, object.id]
)
end
end)
|> Stream.run()
put_stat(:failed_count, failures_count())
persist_state()
force_continue()
end
defp failed_objects_query do
from(o in Object)
|> join(:inner, [o], dmf in fragment("SELECT * FROM data_migration_failed_ids"),
on: dmf.record_id == o.id
)
|> where([_o, dmf], dmf.data_migration_id == ^data_migration_id())
|> order_by([o], asc: o.id)
end
end

View File

@ -183,7 +183,7 @@ def delete_non_create_activities_hashtags do
DELETE FROM hashtags_objects WHERE object_id IN DELETE FROM hashtags_objects WHERE object_id IN
(SELECT DISTINCT objects.id FROM objects (SELECT DISTINCT objects.id FROM objects
JOIN hashtags_objects ON hashtags_objects.object_id = objects.id LEFT JOIN activities JOIN hashtags_objects ON hashtags_objects.object_id = objects.id LEFT JOIN activities
ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = ON associated_object_id(activities) =
(objects.data->>'id') (objects.data->>'id')
AND activities.data->>'type' = 'Create' AND activities.data->>'type' = 'Create'
WHERE activities.id IS NULL); WHERE activities.id IS NULL);

View File

@ -117,9 +117,8 @@ def for_user_query(user, opts \\ %{}) do
|> join(:left, [n, a], object in Object, |> join(:left, [n, a], object in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", "(?->>'id') = associated_object_id(?)",
object.data, object.data,
a.data,
a.data a.data
) )
) )
@ -193,13 +192,11 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility})
|> join(:left, [n, a], mutated_activity in Pleroma.Activity, |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on: on:
fragment( fragment(
"COALESCE((?->'object')->>'id', ?->>'object')", "associated_object_id(?)",
a.data,
a.data a.data
) == ) ==
fragment( fragment(
"COALESCE((?->'object')->>'id', ?->>'object')", "associated_object_id(?)",
mutated_activity.data,
mutated_activity.data mutated_activity.data
) and ) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
@ -385,7 +382,7 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
end end
def create_notifications(%Activity{data: %{"type" => type}} = activity, options) def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
do_create_notifications(activity, options) do_create_notifications(activity, options)
end end
@ -439,6 +436,9 @@ defp type_from_activity(%{data: %{"type" => type}} = activity) do
activity activity
|> type_from_activity_object() |> type_from_activity_object()
"Update" ->
"update"
t -> t ->
raise "No notification type for activity type #{t}" raise "No notification type for activity type #{t}"
end end
@ -513,7 +513,16 @@ def create_poll_notifications(%Activity{} = activity) do
def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do when type in [
"Create",
"Like",
"Announce",
"Follow",
"Move",
"EmojiReact",
"Flag",
"Update"
] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity) potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers = potential_receivers =
@ -553,6 +562,21 @@ def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}
(User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor] (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
end end
# Update activity: notify all who repeated this
def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
repeaters =
Activity.Queries.by_type("Announce")
|> Activity.Queries.by_object_id(object_id)
|> Activity.with_joined_user_actor()
|> where([a, u], u.local)
|> select([a, u], u.ap_id)
|> Repo.all()
repeaters -- [actor]
end
end
def get_potential_receiver_ap_ids(activity) do def get_potential_receiver_ap_ids(activity) do
[] []
|> Utils.maybe_notify_to_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)

View File

@ -40,8 +40,7 @@ def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner)
join(query, join_type, [{object, object_position}], a in Activity, join(query, join_type, [{object, object_position}], a in Activity,
on: on:
fragment( fragment(
"COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ", "associated_object_id(?) = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
a.data, a.data,
object.data, object.data,
a.data, a.data,
@ -208,10 +207,6 @@ def get_cached_by_ap_id(ap_id) do
end end
end end
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{ %ObjectTombstone{
id: id, id: id,

View File

@ -26,8 +26,42 @@ defp touch_changeset(changeset) do
end end
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
has_history? = fn
%{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
_ -> false
end
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields()) internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
remote_history_exists? = has_history?.(new_data)
# If the remote history exists, we treat that as the only source of truth.
new_data =
if has_history?.(old_data) and not remote_history_exists? do
Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
else
new_data
end
# If the remote does not have history information, we need to manage it ourselves
new_data =
if not remote_history_exists? do
changed? =
Pleroma.Constants.status_updatable_fields()
|> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
%{updated_object: updated_object} =
new_data
|> Object.Updater.maybe_update_history(old_data,
updated: changed?,
use_history_in_new_object?: false
)
updated_object
else
new_data
end
Map.merge(new_data, internal_fields) Map.merge(new_data, internal_fields)
end end

View File

@ -0,0 +1,240 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Updater do
require Pleroma.Constants
def update_content_fields(orig_object_data, updated_object) do
Pleroma.Constants.status_updatable_fields()
|> Enum.reduce(
%{data: orig_object_data, updated: false},
fn field, %{data: data, updated: updated} ->
updated =
updated or
(field != "updated" and
Map.get(updated_object, field) != Map.get(orig_object_data, field))
data =
if Map.has_key?(updated_object, field) do
Map.put(data, field, updated_object[field])
else
Map.drop(data, [field])
end
%{data: data, updated: updated}
end
)
end
def maybe_history(object) do
with history <- Map.get(object, "formerRepresentations"),
true <- is_map(history),
"OrderedCollection" <- Map.get(history, "type"),
true <- is_list(Map.get(history, "orderedItems")),
true <- is_integer(Map.get(history, "totalItems")) do
history
else
_ -> nil
end
end
def history_for(object) do
with history when not is_nil(history) <- maybe_history(object) do
history
else
_ -> history_skeleton()
end
end
defp history_skeleton do
%{
"type" => "OrderedCollection",
"totalItems" => 0,
"orderedItems" => []
}
end
def maybe_update_history(
updated_object,
orig_object_data,
opts
) do
updated = opts[:updated]
use_history_in_new_object? = opts[:use_history_in_new_object?]
if not updated do
%{updated_object: updated_object, used_history_in_new_object?: false}
else
# Put edit history
# Note that we may have got the edit history by first fetching the object
{new_history, used_history_in_new_object?} =
with true <- use_history_in_new_object?,
updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
{updated_history, true}
else
_ ->
history = history_for(orig_object_data)
latest_history_item =
orig_object_data
|> Map.drop(["id", "formerRepresentations"])
updated_history =
history
|> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
|> Map.put("totalItems", history["totalItems"] + 1)
{updated_history, false}
end
updated_object =
updated_object
|> Map.put("formerRepresentations", new_history)
%{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
end
end
defp maybe_update_poll(to_be_updated, updated_object) do
choice_key = fn data ->
if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
end
with true <- to_be_updated["type"] == "Question",
key <- choice_key.(updated_object),
true <- key == choice_key.(to_be_updated),
orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
true <- orig_choices == new_choices do
# Choices are the same, but counts are different
to_be_updated
|> Map.put(key, updated_object[key])
else
# Choices (or vote type) have changed, do not allow this
_ -> to_be_updated
end
end
# This calculates the data to be sent as the object of an Update.
# new_data's formerRepresentations is not considered.
# formerRepresentations is added to the returned data.
def make_update_object_data(original_data, new_data, date) do
%{data: updated_data, updated: updated} =
original_data
|> update_content_fields(new_data)
if not updated do
updated_data
else
%{updated_object: updated_data} =
updated_data
|> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
updated_data
|> Map.put("updated", date)
end
end
# This calculates the data of the new Object from an Update.
# new_data's formerRepresentations is considered.
def make_new_object_data_from_update_object(original_data, new_data) do
update_is_reasonable =
with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
{_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
{_, last_updated} when not is_nil(last_updated) <-
{:last_updated, original_data["updated"] || original_data["published"]},
{_, {:ok, last_updated_time, _}} <-
{:last_updated, DateTime.from_iso8601(last_updated)},
:gt <- DateTime.compare(updated_time, last_updated_time) do
:update_everything
else
# only allow poll updates
{:cur_updated, _} -> :no_content_update
:eq -> :no_content_update
# allow all updates
{:last_updated, _} -> :update_everything
# allow no updates
_ -> false
end
%{
updated_object: updated_data,
used_history_in_new_object?: used_history_in_new_object?,
updated: updated
} =
if update_is_reasonable == :update_everything do
%{data: updated_data, updated: updated} =
original_data
|> update_content_fields(new_data)
updated_data
|> maybe_update_history(original_data,
updated: updated,
use_history_in_new_object?: true,
new_data: new_data
)
|> Map.put(:updated, updated)
else
%{
updated_object: original_data,
used_history_in_new_object?: false,
updated: false
}
end
updated_data =
if update_is_reasonable != false do
updated_data
|> maybe_update_poll(new_data)
else
updated_data
end
%{
updated_data: updated_data,
updated: updated,
used_history_in_new_object?: used_history_in_new_object?
}
end
def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
new_items =
Enum.map(items, fun)
|> Enum.reduce_while(
{:ok, []},
fn
{:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
e, _acc -> {:halt, e}
end
)
case new_items do
{:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
e -> e
end
end
def for_each_history_item(history, _, _) do
{:ok, history}
end
def do_with_history(object, fun) do
with history <- object["formerRepresentations"],
object <- Map.drop(object, ["formerRepresentations"]),
{_, {:ok, object}} <- {:main_body, fun.(object)},
{_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
object =
if history do
Map.put(object, "formerRepresentations", history)
else
object
end
{:ok, object}
else
{:main_body, e} -> e
{:history_items, e} -> e
end
end
end

View File

@ -10,17 +10,14 @@ defmodule Pleroma.Signature do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@known_suffixes ["/publickey", "/main-key"]
def key_id_to_actor_id(key_id) do def key_id_to_actor_id(key_id) do
uri = uri =
URI.parse(key_id) key_id
|> URI.parse()
|> Map.put(:fragment, nil) |> Map.put(:fragment, nil)
|> remove_suffix(@known_suffixes)
uri =
if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do
Map.put(uri, :path, String.replace(uri.path, "/publickey", ""))
else
uri
end
maybe_ap_id = URI.to_string(uri) maybe_ap_id = URI.to_string(uri)
@ -36,6 +33,16 @@ def key_id_to_actor_id(key_id) do
end end
end end
defp remove_suffix(uri, [test | rest]) do
if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
Map.put(uri, :path, String.replace(uri.path, test, ""))
else
remove_suffix(uri, rest)
end
end
defp remove_suffix(uri, []), do: uri
def fetch_public_key(conn) do def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
{:ok, actor_id} <- key_id_to_actor_id(kid), {:ok, actor_id} <- key_id_to_actor_id(kid),

View File

@ -36,6 +36,7 @@ defmodule Pleroma.Upload do
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.Web.ActivityPub.Utils
require Logger require Logger
@type source :: @type source ::
@ -99,6 +100,7 @@ def store(upload, opts \\ []) do
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok, {:ok,
%{ %{
"id" => Utils.generate_object_id(),
"type" => opts.activity_type, "type" => opts.activity_type,
"mediaType" => upload.content_type, "mediaType" => upload.content_type,
"url" => [ "url" => [

View File

@ -1574,13 +1574,19 @@ def block(%User{} = blocker, %User{} = blocked) do
blocker blocker
end end
# clear any requested follows as well # clear any requested follows from both sides as well
blocked = blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked {:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked nil -> blocked
end end
blocker =
case CommonAPI.reject_follow_request(blocker, blocked) do
{:ok, %User{} = updated_blocker} -> updated_blocker
nil -> blocker
end
unsubscribe(blocked, blocker) unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true) unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)

View File

@ -91,8 +91,9 @@ def create(relationship_type, %User{} = source, %User{} = target, expires_at \\
expires_at: expires_at expires_at: expires_at
}) })
|> Repo.insert( |> Repo.insert(
on_conflict: {:replace_all_except, [:id]}, on_conflict: {:replace_all_except, [:id, :inserted_at]},
conflict_target: [:source_id, :relationship_type, :target_id] conflict_target: [:source_id, :relationship_type, :target_id],
returning: true
) )
end end

View File

@ -190,7 +190,16 @@ defp insert_activity_with_expiration(data, local, recipients) do
def notify_and_stream(activity) do def notify_and_stream(activity) do
Notification.create_notifications(activity) Notification.create_notifications(activity)
conversation = create_or_bump_conversation(activity, activity.actor) original_activity =
case activity do
%{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} ->
Activity.get_create_by_object_ap_id_with_object(id)
_ ->
activity
end
conversation = create_or_bump_conversation(original_activity, original_activity.actor)
participations = get_participations(conversation) participations = get_participations(conversation)
stream_out(activity) stream_out(activity)
stream_out_participations(participations) stream_out_participations(participations)
@ -256,7 +265,7 @@ def stream_out_participations(_, _), do: :noop
@impl true @impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity) def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete"] do when data_type in ["Create", "Announce", "Delete", "Update"] do
activity activity
|> Topics.get_activity_topics() |> Topics.get_activity_topics()
|> Streamer.stream(activity) |> Streamer.stream(activity)
@ -1150,8 +1159,7 @@ defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do
[activity, object: o] in query, [activity, object: o] in query,
where: where:
fragment( fragment(
"(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)", "(?)->>'type' = 'Create' and associated_object_id((?)) = any (?)",
activity.data,
activity.data, activity.data,
activity.data, activity.data,
^ids ^ids

View File

@ -218,10 +218,16 @@ def like(actor, object) do
end end
end end
# Retricted to user updates for now, always public
@spec update(User.t(), Object.t()) :: {:ok, map(), keyword()} @spec update(User.t(), Object.t()) :: {:ok, map(), keyword()}
def update(actor, object) do def update(actor, object) do
to = [Pleroma.Constants.as_public(), actor.follower_address] {to, cc} =
if object["type"] in Pleroma.Constants.actor_types() do
# User updates, always public
{[Pleroma.Constants.as_public(), actor.follower_address], []}
else
# Status updates, follow the recipients in the object
{object["to"] || [], object["cc"] || []}
end
{:ok, {:ok,
%{ %{
@ -229,7 +235,8 @@ def update(actor, object) do
"type" => "Update", "type" => "Update",
"actor" => actor.ap_id, "actor" => actor.ap_id,
"object" => object, "object" => object,
"to" => to "to" => to,
"cc" => cc
}, []} }, []}
end end

View File

@ -53,10 +53,53 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@required_description_keys [:key, :related_policy] @required_description_keys [:key, :related_policy]
def filter_one(policy, message) do
should_plug_history? =
if function_exported?(policy, :history_awareness, 0) do
policy.history_awareness()
else
:manual
end
|> Kernel.==(:auto)
if not should_plug_history? do
policy.filter(message)
else
main_result = policy.filter(message)
with {_, {:ok, main_message}} <- {:main, main_result},
{_,
%{
"formerRepresentations" => %{
"orderedItems" => [_ | _]
}
}} = {_, object} <- {:object, message["object"]},
{_, {:ok, new_history}} <-
{:history,
Pleroma.Object.Updater.for_each_history_item(
object["formerRepresentations"],
object,
fn item ->
with {:ok, filtered} <- policy.filter(Map.put(message, "object", item)) do
{:ok, filtered["object"]}
else
e -> e
end
end
)} do
{:ok, put_in(main_message, ["object", "formerRepresentations"], new_history)}
else
{:main, _} -> main_result
{:object, _} -> main_result
{:history, e} -> e
end
end
end
def filter(policies, %{} = message) do def filter(policies, %{} = message) do
policies policies
|> Enum.reduce({:ok, message}, fn |> Enum.reduce({:ok, message}, fn
policy, {:ok, message} -> policy.filter(message) policy, {:ok, message} -> filter_one(policy, message)
_, error -> error _, error -> error
end) end)
end end

View File

@ -9,6 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
require Logger require Logger
@impl true
def history_awareness, do: :auto
# has the user successfully posted before? # has the user successfully posted before?
defp old_user?(%User{} = u) do defp old_user?(%User{} = u) do
u.note_count > 0 || u.follower_count > 0 u.note_count > 0 || u.follower_count > 0

View File

@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
def history_awareness, do: :auto
def filter_by_summary( def filter_by_summary(
%{data: %{"summary" => parent_summary}} = _in_reply_to, %{data: %{"summary" => parent_summary}} = _in_reply_to,
%{"summary" => child_summary} = child %{"summary" => child_summary} = child
@ -27,8 +29,8 @@ def filter_by_summary(
def filter_by_summary(_in_reply_to, child), do: child def filter_by_summary(_in_reply_to, child), do: child
def filter(%{"type" => "Create", "object" => child_object} = object) def filter(%{"type" => type, "object" => child_object} = object)
when is_map(child_object) do when type in ["Create", "Update"] and is_map(child_object) do
child = child =
child_object["inReplyTo"] child_object["inReplyTo"]
|> Object.normalize(fetch: false) |> Object.normalize(fetch: false)

View File

@ -11,6 +11,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto
defp do_extract({:a, attrs, _}, acc) do defp do_extract({:a, attrs, _}, acc) do
if Enum.find(attrs, fn {name, value} -> if Enum.find(attrs, fn {name, value} ->
name == "class" && value in ["mention", "u-url mention", "mention u-url"] name == "class" && value in ["mention", "u-url mention", "mention u-url"]
@ -74,11 +77,11 @@ defp clean_recipients(recipients, object) do
@impl true @impl true
def filter( def filter(
%{ %{
"type" => "Create", "type" => type,
"object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to} "object" => %{"type" => "Note", "to" => to, "inReplyTo" => in_reply_to}
} = object } = object
) )
when is_list(to) and is_binary(in_reply_to) do when type in ["Create", "Update"] and is_list(to) and is_binary(in_reply_to) do
# image-only posts from pleroma apparently reach this MRF without the content field # image-only posts from pleroma apparently reach this MRF without the content field
content = object["object"]["content"] || "" content = object["object"]["content"] || ""

View File

@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :manual
defp check_reject(message, hashtags) do defp check_reject(message, hashtags) do
if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do
{:reject, "[HashtagPolicy] Matches with rejected keyword"} {:reject, "[HashtagPolicy] Matches with rejected keyword"}
@ -47,22 +50,46 @@ defp check_ftl_removal(%{"to" => to} = message, hashtags) do
defp check_ftl_removal(message, _hashtags), do: {:ok, message} defp check_ftl_removal(message, _hashtags), do: {:ok, message}
defp check_sensitive(message, hashtags) do defp check_sensitive(message) do
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do {:ok, new_object} =
{:ok, Kernel.put_in(message, ["object", "sensitive"], true)} Object.Updater.do_with_history(message["object"], fn object ->
else hashtags = Object.hashtags(%Object{data: object})
{:ok, message}
end if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
{:ok, Map.put(object, "sensitive", true)}
else
{:ok, object}
end
end)
{:ok, Map.put(message, "object", new_object)}
end end
@impl true @impl true
def filter(%{"type" => "Create", "object" => object} = message) do def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do
hashtags = Object.hashtags(%Object{data: object}) history_items =
with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do
items
else
_ -> []
end
historical_hashtags =
Enum.reduce(history_items, [], fn item, acc ->
acc ++ Object.hashtags(%Object{data: item})
end)
hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags
if hashtags != [] do if hashtags != [] do
with {:ok, message} <- check_reject(message, hashtags), with {:ok, message} <- check_reject(message, hashtags),
{:ok, message} <- check_ftl_removal(message, hashtags), {:ok, message} <-
{:ok, message} <- check_sensitive(message, hashtags) do (if "type" == "Create" do
check_ftl_removal(message, hashtags)
else
{:ok, message}
end),
{:ok, message} <- check_sensitive(message) do
{:ok, message} {:ok, message}
end end
else else

View File

@ -27,24 +27,46 @@ defp object_payload(%{} = object) do
end end
defp check_reject(%{"object" => %{} = object} = message) do defp check_reject(%{"object" => %{} = object} = message) do
payload = object_payload(object) with {:ok, _new_object} <-
Pleroma.Object.Updater.do_with_history(object, fn object ->
payload = object_payload(object)
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern -> if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(payload, pattern) string_matches?(payload, pattern)
end) do end) do
{:reject, "[KeywordPolicy] Matches with rejected keyword"} {:reject, "[KeywordPolicy] Matches with rejected keyword"}
else else
{:ok, message}
end
end) do
{:ok, message} {:ok, message}
else
e -> e
end end
end end
defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
payload = object_payload(object) check_keyword = fn object ->
payload = object_payload(object)
if Pleroma.Constants.as_public() in to and if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
string_matches?(payload, pattern) string_matches?(payload, pattern)
end) do end) do
{:should_delist, nil}
else
{:ok, %{}}
end
end
should_delist? = fn object ->
with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do
false
else
_ -> true
end
end
if Pleroma.Constants.as_public() in to and should_delist?.(object) do
to = List.delete(to, Pleroma.Constants.as_public()) to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []] cc = [Pleroma.Constants.as_public() | message["cc"] || []]
@ -59,8 +81,12 @@ defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
end end
end end
defp check_ftl_removal(message) do
{:ok, message}
end
defp check_replace(%{"object" => %{} = object} = message) do defp check_replace(%{"object" => %{} = object} = message) do
object = replace_kw = fn object ->
["content", "name", "summary"] ["content", "name", "summary"]
|> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end) |> Enum.filter(fn field -> Map.has_key?(object, field) && object[field] end)
|> Enum.reduce(object, fn field, object -> |> Enum.reduce(object, fn field, object ->
@ -73,6 +99,10 @@ defp check_replace(%{"object" => %{} = object} = message) do
Map.put(object, field, data) Map.put(object, field, data)
end) end)
|> (fn object -> {:ok, object} end).()
end
{:ok, object} = Pleroma.Object.Updater.do_with_history(object, replace_kw)
message = Map.put(message, "object", object) message = Map.put(message, "object", object)
@ -80,7 +110,8 @@ defp check_replace(%{"object" => %{} = object} = message) do
end end
@impl true @impl true
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do def filter(%{"type" => type, "object" => %{"content" => _content}} = message)
when type in ["Create", "Update"] do
with {:ok, message} <- check_reject(message), with {:ok, message} <- check_reject(message),
{:ok, message} <- check_ftl_removal(message), {:ok, message} <- check_ftl_removal(message),
{:ok, message} <- check_replace(message) do {:ok, message} <- check_replace(message) do

View File

@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
recv_timeout: 10_000 recv_timeout: 10_000
] ]
@impl true
def history_awareness, do: :auto
defp prefetch(url) do defp prefetch(url) do
# Fetching only proxiable resources # Fetching only proxiable resources
if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do if MediaProxy.enabled?() and MediaProxy.url_proxiable?(url) do
@ -54,10 +57,8 @@ defp preload(%{"object" => %{"attachment" => attachments}} = _message) do
end end
@impl true @impl true
def filter( def filter(%{"type" => type, "object" => %{"attachment" => attachments} = _object} = message)
%{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do
)
when is_list(attachments) and length(attachments) > 0 do
preload(message) preload(message)
{:ok, message} {:ok, message}

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@impl true @impl true
def filter(%{"actor" => actor} = object) do def filter(%{"actor" => actor} = object) do
with true <- is_local?(actor), with true <- is_local?(actor),
true <- is_eligible_type?(object),
true <- is_note?(object), true <- is_note?(object),
false <- has_attachment?(object), false <- has_attachment?(object),
true <- only_mentions?(object) do true <- only_mentions?(object) do
@ -32,7 +33,6 @@ defp is_local?(actor) do
end end
defp has_attachment?(%{ defp has_attachment?(%{
"type" => "Create",
"object" => %{"type" => "Note", "attachment" => attachments} "object" => %{"type" => "Note", "attachment" => attachments}
}) })
when length(attachments) > 0, when length(attachments) > 0,
@ -40,7 +40,13 @@ defp has_attachment?(%{
defp has_attachment?(_), do: false defp has_attachment?(_), do: false
defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "source" => source}}) do defp only_mentions?(%{"object" => %{"type" => "Note", "source" => source}}) do
source =
case source do
%{"content" => text} -> text
_ -> source
end
non_mentions = non_mentions =
source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length source |> String.split() |> Enum.filter(&(not String.starts_with?(&1, "@"))) |> length
@ -53,9 +59,12 @@ defp only_mentions?(%{"type" => "Create", "object" => %{"type" => "Note", "sourc
defp only_mentions?(_), do: false defp only_mentions?(_), do: false
defp is_note?(%{"type" => "Create", "object" => %{"type" => "Note"}}), do: true defp is_note?(%{"object" => %{"type" => "Note"}}), do: true
defp is_note?(_), do: false defp is_note?(_), do: false
defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
defp is_eligible_type?(_), do: false
@impl true @impl true
def describe, do: {:ok, %{}} def describe, do: {:ok, %{}}
end end

View File

@ -6,14 +6,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)" @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def history_awareness, do: :auto
@impl true @impl true
def filter( def filter(
%{ %{
"type" => "Create", "type" => type,
"object" => %{"content" => content, "attachment" => _} = _child_object "object" => %{"content" => content, "attachment" => _} = _child_object
} = object } = object
) )
when content in [".", "<p>.</p>"] do when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do
{:ok, put_in(object, ["object", "content"], "")} {:ok, put_in(object, ["object", "content"], "")}
end end

View File

@ -9,7 +9,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy @behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true @impl true
def filter(%{"type" => "Create", "object" => child_object} = object) do def history_awareness, do: :auto
@impl true
def filter(%{"type" => type, "object" => child_object} = object)
when type in ["Create", "Update"] do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy]) scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
content = content =

View File

@ -12,5 +12,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
label: String.t(), label: String.t(),
description: String.t() description: String.t()
} }
@optional_callbacks config_description: 0 @callback history_awareness() :: :auto | :manual
@optional_callbacks config_description: 0, history_awareness: 0
end end

View File

@ -103,8 +103,8 @@ def validate(
meta meta
) )
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
with {:ok, object_data} <- cast_and_apply(object), with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), meta = Keyword.put(meta, :object_data, object_data),
{:ok, create_activity} <- {:ok, create_activity} <-
create_activity create_activity
|> CreateGenericValidator.cast_and_validate(meta) |> CreateGenericValidator.cast_and_validate(meta)
@ -128,19 +128,53 @@ def validate(%{"type" => type} = object, meta)
end end
with {:ok, object} <- with {:ok, object} <-
object do_separate_with_history(object, fn object ->
|> validator.cast_and_validate() with {:ok, object} <-
|> Ecto.Changeset.apply_action(:insert) do object
object = stringify_keys(object) |> validator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
# Insert copy of hashtags as strings for the non-hashtag table indexing # Insert copy of hashtags as strings for the non-hashtag table indexing
tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object}) tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
object = Map.put(object, "tag", tag) object = Map.put(object, "tag", tag)
{:ok, object}
end
end) do
{:ok, object, meta} {:ok, object, meta}
end end
end end
def validate(
%{"type" => "Update", "object" => %{"type" => objtype} = object} = update_activity,
meta
)
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
with {_, false} <- {:local, Access.get(meta, :local, false)},
{_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)},
meta = Keyword.put(meta, :object_data, object_data),
{:ok, update_activity} <-
update_activity
|> UpdateValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
update_activity = stringify_keys(update_activity)
{:ok, update_activity, meta}
else
{:local, _} ->
with {:ok, object} <-
update_activity
|> UpdateValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
{:object_validation, e} ->
e
end
end
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer] do ChatMessage Answer] do
@ -178,6 +212,15 @@ def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do
def validate(o, m), do: {:error, {:validator_not_set, {o, m}}} def validate(o, m), do: {:error, {:validator_not_set, {o, m}}}
def cast_and_apply_and_stringify_with_history(object) do
do_separate_with_history(object, fn object ->
with {:ok, object_data} <- cast_and_apply(object),
object_data <- object_data |> stringify_keys() do
{:ok, object_data}
end
end)
end
def cast_and_apply(%{"type" => "ChatMessage"} = object) do def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object) ChatMessageValidator.cast_and_apply(object)
end end
@ -236,4 +279,54 @@ def fetch_actor_and_object(object) do
Object.normalize(object["object"], fetch: true) Object.normalize(object["object"], fetch: true)
:ok :ok
end end
defp for_each_history_item(
%{"type" => "OrderedCollection", "orderedItems" => items} = history,
object,
fun
) do
processed_items =
Enum.map(items, fn item ->
with item <- Map.put(item, "id", object["id"]),
{:ok, item} <- fun.(item) do
item
else
_ -> nil
end
end)
if Enum.all?(processed_items, &(not is_nil(&1))) do
{:ok, Map.put(history, "orderedItems", processed_items)}
else
{:error, :invalid_history}
end
end
defp for_each_history_item(nil, _object, _fun) do
{:ok, nil}
end
defp for_each_history_item(_, _object, _fun) do
{:error, :invalid_history}
end
# fun is (object -> {:ok, validated_object_with_string_keys})
defp do_separate_with_history(object, fun) do
with history <- object["formerRepresentations"],
object <- Map.drop(object, ["formerRepresentations"]),
{_, {:ok, object}} <- {:main_body, fun.(object)},
{_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
object =
if history do
Map.put(object, "formerRepresentations", history)
else
object
end
{:ok, object}
else
{:main_body, e} -> e
{:history_items, e} -> e
end
end
end end

View File

@ -49,7 +49,10 @@ defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data
defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"]) defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
defp fix_url(data), do: data defp fix_url(data), do: data
defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
Map.put(data, "tag", Enum.filter(tag, &is_map/1))
end
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag]) defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"]) defp fix_tag(data), do: Map.drop(data, ["tag"])
@ -60,7 +63,10 @@ defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies), defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies) do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies), # TODO: Pleroma does not have any support for Collections at the moment.
# If the `replies` field is not something the ObjectID validator can handle,
# the activity/object would be rejected, which is bad behavior.
defp fix_replies(%{"replies" => replies} = data) when not is_list(replies),
do: Map.drop(data, ["replies"]) do: Map.drop(data, ["replies"])
defp fix_replies(data), do: data defp fix_replies(data), do: data
@ -94,7 +100,7 @@ def changeset(struct, data) do
defp validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Article", "Note", "Page"]) |> validate_inclusion(:type, ["Article", "Note", "Page"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence()

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field(:id, :string)
field(:type, :string) field(:type, :string)
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream") field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:name, :string) field(:name, :string)
@ -43,7 +44,7 @@ def changeset(struct, data) do
|> fix_url() |> fix_url()
struct struct
|> cast(data, [:type, :mediaType, :name, :blurhash]) |> cast(data, [:id, :type, :mediaType, :name, :blurhash])
|> cast_embed(:url, with: &url_changeset/2) |> cast_embed(:url, with: &url_changeset/2)
|> validate_inclusion(:type, ~w[Link Document Audio Image Video]) |> validate_inclusion(:type, ~w[Link Document Audio Image Video])
|> validate_required([:type, :mediaType, :url]) |> validate_required([:type, :mediaType, :url])

View File

@ -33,6 +33,7 @@ defmacro object_fields do
field(:content, :string) field(:content, :string)
field(:published, ObjectValidators.DateTime) field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)
field(:emoji, ObjectValidators.Emoji, default: %{}) field(:emoji, ObjectValidators.Emoji, default: %{})
embeds_many(:attachment, AttachmentValidator) embeds_many(:attachment, AttachmentValidator)
end end
@ -51,8 +52,6 @@ defmacro status_object_fields do
field(:summary, :string) field(:summary, :string)
field(:context, :string) field(:context, :string)
# short identifier for PleromaFE to group statuses by context
field(:context_id, :integer)
field(:sensitive, :boolean, default: false) field(:sensitive, :boolean, default: false)
field(:replies_count, :integer, default: 0) field(:replies_count, :integer, default: 0)

View File

@ -22,14 +22,15 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba
end end
def fix_object_defaults(data) do def fix_object_defaults(data) do
%{data: %{"id" => context}, id: context_id} = context =
Utils.create_context(data["context"] || data["conversation"]) Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
)
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"]) %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])
data data
|> Map.put("context", context) |> Map.put("context", context)
|> Map.put("context_id", context_id)
|> cast_and_filter_recipients("to", follower_collection) |> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection) |> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection) |> cast_and_filter_recipients("bto", follower_collection)

View File

@ -75,7 +75,7 @@ def fix(data, meta) do
data data
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
|> Map.put_new("context", object["context"]) |> Map.put("context", object["context"])
|> fix_addressing(object) |> fix_addressing(object)
end end

View File

@ -62,7 +62,7 @@ def changeset(struct, data) do
defp validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Event"]) |> validate_inclusion(:type, ["Event"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence()

View File

@ -80,7 +80,7 @@ def changeset(struct, data) do
defp validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Question"]) |> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence()

View File

@ -51,7 +51,9 @@ def validate_updating_rights(cng) do
with actor = get_field(cng, :actor), with actor = get_field(cng, :actor),
object = get_field(cng, :object), object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
true <- actor == object_id do actor_uri <- URI.parse(actor),
object_uri <- URI.parse(object_id),
true <- actor_uri.host == object_uri.host do
cng cng
else else
_e -> _e ->

View File

@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Workers.PollWorker alias Pleroma.Workers.PollWorker
require Pleroma.Constants
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex) @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@ -153,23 +154,26 @@ def handle(
# Tasks this handles: # Tasks this handles:
# - Update the user # - Update the user
# - Update a non-user object (Note, Question, etc.)
# #
# For a local user, we also get a changeset with the full information, so we # For a local user, we also get a changeset with the full information, so we
# can update non-federating, non-activitypub settings as well. # can update non-federating, non-activitypub settings as well.
@impl true @impl true
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
if changeset = Keyword.get(meta, :user_update_changeset) do updated_object_id = updated_object["id"]
changeset
|> User.update_and_set_cache() with {_, true} <- {:has_id, is_binary(updated_object_id)},
%{"type" => type} <- updated_object,
{_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do
if is_user do
handle_update_user(object, meta)
else
handle_update_object(object, meta)
end
else else
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object) _ ->
{:ok, object, meta}
User.get_by_ap_id(updated_object["id"])
|> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
end end
{:ok, object, meta}
end end
# Tasks this handles: # Tasks this handles:
@ -390,6 +394,79 @@ def handle(object, meta) do
{:ok, object, meta} {:ok, object, meta}
end end
defp handle_update_user(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
) do
if changeset = Keyword.get(meta, :user_update_changeset) do
changeset
|> User.update_and_set_cache()
else
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
User.get_by_ap_id(updated_object["id"])
|> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
end
{:ok, object, meta}
end
defp handle_update_object(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
) do
orig_object_ap_id = updated_object["id"]
orig_object = Object.get_by_ap_id(orig_object_ap_id)
orig_object_data = orig_object.data
updated_object =
if meta[:local] do
# If this is a local Update, we don't process it by transmogrifier,
# so we use the embedded object as-is.
updated_object
else
meta[:object_data]
end
if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do
%{
updated_data: updated_object_data,
updated: updated,
used_history_in_new_object?: used_history_in_new_object?
} = Object.Updater.make_new_object_data_from_update_object(orig_object_data, updated_object)
changeset =
orig_object
|> Repo.preload(:hashtags)
|> Object.change(%{data: updated_object_data})
with {:ok, new_object} <- Repo.update(changeset),
{:ok, _} <- Object.invalid_object_cache(new_object),
{:ok, _} <- Object.set_cache(new_object),
# The metadata/utils.ex uses the object id for the cache.
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
if used_history_in_new_object? do
with create_activity when not is_nil(create_activity) <-
Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
nil
else
_ -> nil
end
end
if updated do
object
|> Activity.normalize()
|> ActivityPub.notify_and_stream()
end
end
end
{:ok, object, meta}
end
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
actor = User.get_cached_by_ap_id(object.data["actor"]) actor = User.get_cached_by_ap_id(object.data["actor"])

View File

@ -687,6 +687,24 @@ def prepare_object(object) do
|> strip_internal_fields |> strip_internal_fields
|> strip_internal_tags |> strip_internal_tags
|> set_type |> set_type
|> maybe_process_history
end
defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do
processed_history =
Enum.map(
history,
fn
item when is_map(item) -> prepare_object(item)
item -> item
end
)
put_in(object, ["formerRepresentations", "orderedItems"], processed_history)
end
defp maybe_process_history(object) do
object
end end
# @doc # @doc
@ -711,6 +729,21 @@ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do
object =
object
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object = object =
object_id object_id

View File

@ -154,22 +154,7 @@ def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false) Notification.get_notified_from_activity(%Activity{data: object}, false)
end end
def create_context(context) do def maybe_create_context(context), do: context || generate_id("contexts")
context = context || generate_id("contexts")
# Ecto has problems accessing the constraint inside the jsonb,
# so we explicitly check for the existed object before insert
object = Object.get_cached_by_ap_id(context)
with true <- is_nil(object),
changeset <- Object.context_mapping(context),
{:ok, inserted_object} <- Repo.insert(changeset) do
inserted_object
else
_ ->
object
end
end
@doc """ @doc """
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
@ -201,18 +186,16 @@ def lazy_put_activity_defaults(map, true) do
|> Map.put_new("id", "pleroma:fakeid") |> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0) |> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext") |> Map.put_new("context", "pleroma:fakecontext")
|> Map.put_new("context_id", -1)
|> lazy_put_object_defaults(true) |> lazy_put_object_defaults(true)
end end
def lazy_put_activity_defaults(map, _fake?) do def lazy_put_activity_defaults(map, _fake?) do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"]) context = maybe_create_context(map["context"])
map map
|> Map.put_new_lazy("id", &generate_activity_id/0) |> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0) |> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context) |> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
|> lazy_put_object_defaults(false) |> lazy_put_object_defaults(false)
end end
@ -226,7 +209,6 @@ defp lazy_put_object_defaults(%{"object" => map} = activity, true)
|> Map.put_new("id", "pleroma:fake_object_id") |> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new_lazy("published", &make_date/0) |> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"]) |> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
|> Map.put_new("fake", true) |> Map.put_new("fake", true)
%{activity | "object" => object} %{activity | "object" => object}
@ -239,7 +221,6 @@ defp lazy_put_object_defaults(%{"object" => map} = activity, _)
|> Map.put_new_lazy("id", &generate_object_id/0) |> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0) |> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"]) |> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
%{activity | "object" => object} %{activity | "object" => object}
end end

View File

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaSettingsOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Settings"],
summary: "Get settings for an application",
description: "Get synchronized settings for an application",
operationId: "SettingsController.show",
parameters: [app_name_param()],
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => Operation.response("object", "application/json", object())
}
}
end
def update_operation do
%Operation{
tags: ["Settings"],
summary: "Update settings for an application",
description: "Update synchronized settings for an application",
operationId: "SettingsController.update",
parameters: [app_name_param()],
security: [%{"oAuth" => ["write:accounts"]}],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("object", "application/json", object())
}
}
end
def app_name_param do
Operation.parameter(:app, :path, %Schema{type: :string}, "Application name",
example: "pleroma-fe",
required: true
)
end
def object do
%Schema{
title: "Settings object",
description: "The object that contains settings for the application.",
type: :object
}
end
def update_request do
%Schema{
title: "SettingsUpdateRequest",
type: :object,
description:
"The settings object to be merged with the current settings. To remove a field, set it to null.",
example: %{
"config1" => true,
"config2_to_unset" => nil
}
}
end
end

View File

@ -6,9 +6,13 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
alias Pleroma.Web.ApiSpec.Schemas.Status alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@ -434,6 +438,59 @@ def bookmarks_operation do
} }
end end
def show_history_operation do
%Operation{
tags: ["Retrieve status history"],
summary: "Status history",
description: "View history of a status",
operationId: "StatusController.show_history",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
id_param()
],
responses: %{
200 => status_history_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def show_source_operation do
%Operation{
tags: ["Retrieve status source"],
summary: "Status source",
description: "View source of a status",
operationId: "StatusController.show_source",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
id_param()
],
responses: %{
200 => status_source_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Update status"],
summary: "Update status",
description: "Change the content of a status",
operationId: "StatusController.update",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [
id_param()
],
requestBody: request_body("Parameters", update_request(), required: true),
responses: %{
200 => status_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def array_of_statuses do def array_of_statuses do
%Schema{type: :array, items: Status, example: [Status.schema().example]} %Schema{type: :array, items: Status, example: [Status.schema().example]}
end end
@ -537,6 +594,60 @@ defp create_request do
} }
end end
defp update_request do
%Schema{
title: "StatusUpdateRequest",
type: :object,
properties: %{
status: %Schema{
type: :string,
nullable: true,
description:
"Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
},
media_ids: %Schema{
nullable: true,
type: :array,
items: %Schema{type: :string},
description: "Array of Attachment ids to be attached as media."
},
poll: poll_params(),
sensitive: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Mark status and attached media as sensitive?"
},
spoiler_text: %Schema{
type: :string,
nullable: true,
description:
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
},
content_type: %Schema{
type: :string,
nullable: true,
description:
"The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
},
to: %Schema{
type: :array,
nullable: true,
items: %Schema{type: :string},
description:
"A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
}
},
example: %{
"status" => "What time is it?",
"sensitive" => "false",
"poll" => %{
"options" => ["Cofe", "Adventure"],
"expires_in" => 420
}
}
}
end
def poll_params do def poll_params do
%Schema{ %Schema{
nullable: true, nullable: true,
@ -579,6 +690,87 @@ defp status_response do
Operation.response("Status", "application/json", Status) Operation.response("Status", "application/json", Status)
end end
defp status_history_response do
Operation.response(
"Status History",
"application/json",
%Schema{
title: "Status history",
description: "Response schema for history of a status",
type: :array,
items: %Schema{
type: :object,
properties: %{
account: %Schema{
allOf: [Account],
description: "The account that authored this status"
},
content: %Schema{
type: :string,
format: :html,
description: "HTML-encoded status content"
},
sensitive: %Schema{
type: :boolean,
description: "Is this status marked as sensitive content?"
},
spoiler_text: %Schema{
type: :string,
description:
"Subject or summary line, below which status content is collapsed until expanded"
},
created_at: %Schema{
type: :string,
format: "date-time",
description: "The date when this status was created"
},
media_attachments: %Schema{
type: :array,
items: Attachment,
description: "Media that is attached to this status"
},
emojis: %Schema{
type: :array,
items: Emoji,
description: "Custom emoji to be used when rendering status content"
},
poll: %Schema{
allOf: [Poll],
nullable: true,
description: "The poll attached to the status"
}
}
}
}
)
end
defp status_source_response do
Operation.response(
"Status Source",
"application/json",
%Schema{
type: :object,
properties: %{
id: FlakeID,
text: %Schema{
type: :string,
description: "Raw source of status content"
},
spoiler_text: %Schema{
type: :string,
description:
"Subject or summary line, below which status content is collapsed until expanded"
},
content_type: %Schema{
type: :string,
description: "The content type of the source"
}
}
}
)
end
defp context do defp context do
%Schema{ %Schema{
title: "StatusContext", title: "StatusContext",

View File

@ -405,6 +405,16 @@ defp remote_interaction_request do
} }
end end
def show_subscribe_form_operation do
%Operation{
tags: ["Accounts"],
summary: "Show remote subscribe form",
operationId: "UtilController.show_subscribe_form",
parameters: [],
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
}
end
defp delete_account_request do defp delete_account_request do
%Schema{ %Schema{
title: "AccountDeleteRequest", title: "AccountDeleteRequest",

View File

@ -73,6 +73,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
format: "date-time", format: "date-time",
description: "The date when this status was created" description: "The date when this status was created"
}, },
edited_at: %Schema{
type: :string,
format: "date-time",
nullable: true,
description: "The date when this status was last edited"
},
emojis: %Schema{ emojis: %Schema{
type: :array, type: :array,
items: Emoji, items: Emoji,
@ -142,9 +148,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
description: description:
"A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`" "A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
}, },
context: %Schema{
type: :string,
description: "The thread identifier the status is associated with"
},
conversation_id: %Schema{ conversation_id: %Schema{
type: :integer, type: :integer,
description: "The ID of the AP context the status is associated with (if any)" deprecated: true,
description:
"The ID of the AP context the status is associated with (if any); deprecated, please use `context` instead"
}, },
direct_conversation_id: %Schema{ direct_conversation_id: %Schema{
type: :integer, type: :integer,
@ -319,6 +331,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
"pinned" => false, "pinned" => false,
"pleroma" => %{ "pleroma" => %{
"content" => %{"text/plain" => "foobar"}, "content" => %{"text/plain" => "foobar"},
"context" => "http://localhost:4001/objects/8b4c0c80-6a37-4d2a-b1b9-05a19e3875aa",
"conversation_id" => 345_972, "conversation_id" => 345_972,
"direct_conversation_id" => nil, "direct_conversation_id" => nil,
"emoji_reactions" => [], "emoji_reactions" => [],

View File

@ -402,6 +402,41 @@ def post(user, %{status: _} = data) do
end end
end end
def update(user, orig_activity, changes) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes),
{:ok, update_data, _} <- Builder.update(user, new_object),
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
{:ok, update}
else
_ -> {:error, nil}
end
end
defp make_update_data(user, orig_object, changes) do
kept_params = %{
visibility: Visibility.get_visibility(orig_object),
in_reply_to_id:
with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
activity_id
else
_ -> nil
end
}
params = Map.merge(changes, kept_params)
with {:ok, draft} <- ActivityDraft.create(user, params) do
change =
Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
{:ok, change}
else
_ -> {:error, nil}
end
end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def pin(id, %User{} = user) do def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),

View File

@ -224,7 +224,10 @@ defp object(draft) do
object = object =
note_data note_data
|> Map.put("emoji", emoji) |> Map.put("emoji", emoji)
|> Map.put("source", draft.status) |> Map.put("source", %{
"content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type])
})
|> Map.put("generator", draft.params[:generator]) |> Map.put("generator", draft.params[:generator])
%__MODULE__{draft | object: object} %__MODULE__{draft | object: object}

View File

@ -37,7 +37,7 @@ def attachments_from_ids_no_descs([]), do: []
def attachments_from_ids_no_descs(ids) do def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id -> Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do case get_attachment(media_id) do
%Object{data: data} -> data %Object{data: data} -> data
_ -> nil _ -> nil
end end
@ -51,13 +51,17 @@ def attachments_from_ids_descs(ids, descs_str) do
{_, descs} = Jason.decode(descs_str) {_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id -> Enum.map(ids, fn media_id ->
with %Object{data: data} <- Repo.get(Object, media_id) do with %Object{data: data} <- get_attachment(media_id) do
Map.put(data, "name", descs[media_id]) Map.put(data, "name", descs[media_id])
end end
end) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
end end
defp get_attachment(media_id) do
Repo.get(Object, media_id)
end
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())} @spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
@ -219,7 +223,7 @@ def make_content_html(%ActivityDraft{} = draft) do
|> maybe_add_attachments(draft.attachments, attachment_links) |> maybe_add_attachments(draft.attachments, attachment_links)
end end
defp get_content_type(content_type) do def get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type content_type
else else
@ -449,35 +453,6 @@ def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
def get_report_statuses(_, _), do: {:ok, nil} def get_report_statuses(_, _), do: {:ok, nil}
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, dgettext("errors", "No such conversation")}
end
end
def validate_character_limit("" = _full_payload, [] = _attachments) do def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")} {:error, dgettext("errors", "Cannot post an empty status without attachments")}
end end

View File

@ -51,6 +51,7 @@ def index(conn, %{account_id: account_id} = params) do
move move
pleroma:emoji_reaction pleroma:emoji_reaction
poll poll
update
} }
def index(%{assigns: %{user: user}} = conn, params) do def index(%{assigns: %{user: user}} = conn, params) do
params = params =

View File

@ -38,7 +38,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:index, :index,
:show, :show,
:card, :card,
:context :context,
:show_history,
:show_source
] ]
) )
@ -49,7 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
:create, :create,
:delete, :delete,
:reblog, :reblog,
:unreblog :unreblog,
:update
] ]
) )
@ -191,6 +194,59 @@ def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = c
create(%Plug.Conn{conn | body_params: params}, %{}) create(%Plug.Conn{conn | body_params: params}, %{})
end end
@doc "GET /api/v1/statuses/:id/history"
def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "history.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id/source"
def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "source.json",
activity: activity,
for: user
)
else
_ -> {:error, :not_found}
end
end
@doc "PUT /api/v1/statuses/:id"
def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
{_, true} <- {:is_create, activity.data["type"] == "Create"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
changes <- body_params |> put_application(conn),
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
{:own_status, _} -> {:error, :forbidden}
{:pipeline, _} -> {:error, :internal_server_error}
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id" @doc "GET /api/v1/statuses/:id"
def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),

View File

@ -69,6 +69,7 @@ def features do
"shareable_emoji_packs", "shareable_emoji_packs",
"multifetch", "multifetch",
"pleroma:api/v1/notifications:include_types_filter", "pleroma:api/v1/notifications:include_types_filter",
"editing",
if Config.get([:activitypub, :blockers_visible]) do if Config.get([:activitypub, :blockers_visible]) do
"blockers_visible" "blockers_visible"
end, end,
@ -98,7 +99,8 @@ def features do
end, end,
if Config.get([:instance, :profile_directory]) do if Config.get([:instance, :profile_directory]) do
"profile_directory" "profile_directory"
end end,
"pleroma:get:main/ostatus"
] ]
|> Enum.filter(& &1) |> Enum.filter(& &1)
end end

View File

@ -19,7 +19,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
@parent_types ~w{Like Announce EmojiReact} defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
defp object_id_for(%{data: %{"object" => id}}) when is_binary(id), do: id
@parent_types ~w{Like Announce EmojiReact Update}
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
activities = Enum.map(notifications, & &1.activity) activities = Enum.map(notifications, & &1.activity)
@ -30,7 +34,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
%{data: %{"type" => type}} -> %{data: %{"type" => type}} ->
type in @parent_types type in @parent_types
end) end)
|> Enum.map(& &1.data["object"]) |> Enum.map(&object_id_for/1)
|> Activity.create_by_object_ap_id() |> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left) |> Activity.with_preloaded_object(:left)
|> Pleroma.Repo.all() |> Pleroma.Repo.all()
@ -78,9 +82,9 @@ def render(
parent_activity_fn = fn -> parent_activity_fn = fn ->
if opts[:parent_activities] do if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"]) Activity.Queries.find_by_object_ap_id(opts[:parent_activities], object_id_for(activity))
else else
Activity.get_create_by_object_ap_id(activity.data["object"]) Activity.get_create_by_object_ap_id(object_id_for(activity))
end end
end end
@ -109,6 +113,9 @@ def render(
"reblog" -> "reblog" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts) put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"update" ->
put_status(response, parent_activity_fn.(), reading_user, status_render_opts)
"move" -> "move" ->
put_target(response, activity, reading_user, %{}) put_target(response, activity, reading_user, %{})

View File

@ -57,11 +57,19 @@ defp get_replied_to_activities(activities) do
end) end)
end end
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id), # DEPRECATED This field seems to be a left-over from the StatusNet era.
do: context_id # If your application uses `pleroma.conversation_id`: this field is deprecated.
# It is currently stubbed instead by doing a CRC32 of the context, and
# clearing the MSB to avoid overflow exceptions with signed integers on the
# different clients using this field (Java/Kotlin code, mostly; see Husky.)
# This should be removed in a future version of Pleroma. Pleroma-FE currently
# depends on this field, as well.
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
use Bitwise
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context), :erlang.crc32(context)
do: Utils.context_to_conversation_id(context) |> band(bnot(0x8000_0000))
end
defp get_context_id(_), do: nil defp get_context_id(_), do: nil
@ -258,10 +266,30 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
created_at = Utils.to_masto_date(object.data["published"]) created_at = Utils.to_masto_date(object.data["published"])
edited_at =
with %{"updated" => updated} <- object.data,
date <- Utils.to_masto_date(updated),
true <- date != "" do
date
else
_ ->
nil
end
reply_to = get_reply_to(activity, opts) reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"]) reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
history_len =
1 +
(Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> length())
# See render("history.json", ...) for more details
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
content = content =
object object
|> render_content() |> render_content()
@ -271,14 +299,14 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|> Activity.HTML.get_cached_scrubbed_html_for_activity( |> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]), User.html_filter_policy(opts[:for]),
activity, activity,
"mastoapi:content" "mastoapi:content:#{chrono_order}"
) )
content_plaintext = content_plaintext =
content content
|> Activity.HTML.get_cached_stripped_html_for_activity( |> Activity.HTML.get_cached_stripped_html_for_activity(
activity, activity,
"mastoapi:content" "mastoapi:content:#{chrono_order}"
) )
summary = object.data["summary"] || "" summary = object.data["summary"] || ""
@ -344,8 +372,9 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
reblog: nil, reblog: nil,
card: card, card: card,
content: content_html, content: content_html,
text: opts[:with_source] && object.data["source"], text: opts[:with_source] && get_source_text(object.data["source"]),
created_at: created_at, created_at: created_at,
edited_at: edited_at,
reblogs_count: announcement_count, reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0, replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count, favourites_count: like_count,
@ -367,6 +396,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
pleroma: %{ pleroma: %{
local: activity.local, local: activity.local,
conversation_id: get_context_id(activity), conversation_id: get_context_id(activity),
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext}, content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary}, spoiler_text: %{"text/plain" => summary},
@ -384,6 +414,100 @@ def render("show.json", _) do
nil nil
end end
def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity, fetch: false)
hashtags = Object.hashtags(object)
user = CommonAPI.get_user(activity.data["actor"])
past_history =
Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> Enum.map(&Map.put(&1, "id", object.data["id"]))
|> Enum.map(&%Object{data: &1, id: object.id})
history =
[object | past_history]
# Mastodon expects the original to be at the first
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map(fn {object, chrono_order} ->
%{
# The history is prepended every time there is a new edit.
# In chrono_order, the oldest item is always at 0, and so on.
# The chrono_order is an invariant kept between edits.
chrono_order: chrono_order,
object: object
}
end)
individual_opts =
opts
|> Map.put(:as, :item)
|> Map.put(:user, user)
|> Map.put(:hashtags, hashtags)
render_many(history, StatusView, "history_item.json", individual_opts)
end
def render(
"history_item.json",
%{
activity: activity,
user: user,
item: %{object: object, chrono_order: chrono_order},
hashtags: hashtags
} = opts
) do
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
content =
object
|> render_content()
content_html =
content
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content:#{chrono_order}"
)
summary = object.data["summary"] || ""
%{
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
content: content_html,
sensitive: sensitive,
spoiler_text: summary,
created_at: created_at,
media_attachments: attachments,
emojis: build_emojis(object.data["emoji"]),
poll: render(PollView, "show.json", object: object, for: opts[:for])
}
end
def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
object = Object.normalize(activity, fetch: false)
%{
id: activity.id,
text: get_source_text(Map.get(object.data, "source", "")),
spoiler_text: Map.get(object.data, "summary", ""),
content_type: get_source_content_type(object.data["source"])
}
end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url_data = URI.parse(page_url) page_url_data = URI.parse(page_url)
@ -436,10 +560,19 @@ def render("attachment.json", %{attachment: attachment}) do
true -> "unknown" true -> "unknown"
end end
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href) attachment_id =
with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
{_, %Object{data: _object_data, id: object_id}} <-
{:object, Object.get_by_ap_id(ap_id)} do
to_string(object_id)
else
_ ->
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
to_string(attachment["id"] || hash_id)
end
%{ %{
id: to_string(attachment["id"] || hash_id), id: attachment_id,
url: href, url: href,
remote_url: href, remote_url: href,
preview_url: href_preview, preview_url: href_preview,
@ -601,4 +734,24 @@ defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
end end
defp build_image_url(_, _), do: nil defp build_image_url(_, _), do: nil
defp get_source_text(%{"content" => content} = _source) do
content
end
defp get_source_text(source) when is_binary(source) do
source
end
defp get_source_text(_) do
""
end
defp get_source_content_type(%{"mediaType" => type} = _source) do
type
end
defp get_source_content_type(_source) do
Utils.get_content_type(nil)
end
end end

View File

@ -54,7 +54,7 @@ defp handle_preview(conn, url) do
media_proxy_url = MediaProxy.url(url) media_proxy_url = MediaProxy.url(url)
with {:ok, %{status: status} = head_response} when status in 200..299 <- with {:ok, %{status: status} = head_response} when status in 200..299 <-
Pleroma.HTTP.request("head", media_proxy_url, [], [], pool: :media) do Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], pool: :media) do
content_type = Tesla.get_header(head_response, "content-type") content_type = Tesla.get_header(head_response, "content-type")
content_length = Tesla.get_header(head_response, "content-length") content_length = Tesla.get_header(head_response, "content-length")
content_length = content_length && String.to_integer(content_length) content_length = content_length && String.to_integer(content_length)

View File

@ -8,8 +8,8 @@ defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.HTML alias Pleroma.HTML
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do defp scrub_html_and_truncate_object_field(field, object) do
content field
# html content comes from DB already encoded, decode first and scrub after # html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode() |> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ") |> String.replace(~r/<br\s?\/?>/, " ")
@ -19,6 +19,17 @@ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
|> Formatter.truncate() |> Formatter.truncate()
end end
def scrub_html_and_truncate(%{data: %{"summary" => summary}} = object)
when is_binary(summary) and summary != "" do
summary
|> scrub_html_and_truncate_object_field(object)
end
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
content
|> scrub_html_and_truncate_object_field(object)
end
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
content content
|> scrub_html |> scrub_html

View File

@ -0,0 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.SettingsController do
use Pleroma.Web, :controller
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]} when action in [:update]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]} when action in [:show]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaSettingsOperation
@doc "GET /api/v1/pleroma/settings/:app"
def show(%{assigns: %{user: user}} = conn, %{app: app} = _params) do
conn
|> json(get_settings(user, app))
end
@doc "PATCH /api/v1/pleroma/settings/:app"
def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{app: app} = _params) do
settings =
get_settings(user, app)
|> merge_recursively(body_params)
with changeset <-
Pleroma.User.update_changeset(
user,
%{pleroma_settings_store: %{app => settings}}
),
{:ok, _} <- Pleroma.Repo.update(changeset) do
conn
|> json(settings)
end
end
defp merge_recursively(old, %{} = new) do
old = ensure_object(old)
Enum.reduce(
new,
old,
fn
{k, nil}, acc ->
Map.drop(acc, [k])
{k, %{} = new_child}, acc ->
Map.put(acc, k, merge_recursively(acc[k], new_child))
{k, v}, acc ->
Map.put(acc, k, v)
end
)
end
defp get_settings(user, app) do
user.pleroma_settings_store
|> Map.get(app, %{})
|> ensure_object()
end
defp ensure_object(%{} = object) do
object
end
defp ensure_object(_) do
%{}
end
end

View File

@ -25,21 +25,58 @@ def call(conn, _opts) do
end end
end end
defp validate_signature(conn, request_target) do
# Newer drafts for HTTP signatures now use @request-target instead of the
# old (request-target). We'll now support both for incoming signatures.
conn =
conn
|> put_req_header("(request-target)", request_target)
|> put_req_header("@request-target", request_target)
HTTPSignatures.validate_conn(conn)
end
defp validate_signature(conn) do
# This (request-target) is non-standard, but many implementations do it
# this way due to a misinterpretation of
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-06
# "path" was interpreted as not having the query, though later examples
# show that it must be the absolute path + query. This behavior is kept to
# make sure most software (Pleroma itself, Mastodon, and probably others)
# do not break.
request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
# This is the proper way to build the @request-target, as expected by
# many HTTP signature libraries, clarified in the following draft:
# https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#section-2.2.6
# It is the same as before, but containing the query part as well.
proper_target = request_target <> "?#{conn.query_string}"
cond do
# Normal, non-standard behavior but expected by Pleroma and more.
validate_signature(conn, request_target) ->
true
# Has query string and the previous one failed: let's try the standard.
conn.query_string != "" ->
validate_signature(conn, proper_target)
# If there's no query string and signature fails, it's rotten.
true ->
false
end
end
defp maybe_assign_valid_signature(conn) do defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do if has_signature_header?(conn) do
# set (request-target) header to the appropriate value # we replace the digest header with the one we computed in DigestPlug
# we also replace the digest header with the one we computed
request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
conn = conn =
conn case conn do
|> put_req_header("(request-target)", request_target)
|> case do
%{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest) %{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
conn -> conn conn -> conn
end end
assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) assign(conn, :valid_signature, validate_signature(conn))
else else
Logger.debug("No signature header!") Logger.debug("No signature header!")
conn conn

View File

@ -47,15 +47,17 @@ def call(conn, _) do
# #
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do defp fetch_user_and_token(token) do
query = token_query =
from(t in Token, from(t in Token,
where: t.token == ^token, where: t.token == ^token
join: user in assoc(t, :user),
preload: [user: user]
) )
with %Token{user: user} = token_record <- Repo.one(query) do with %Token{user_id: user_id} = token_record <- Repo.one(token_query),
false <- is_nil(user_id),
%User{} = user <- User.get_cached_by_id(user_id) do
{:ok, user, token_record} {:ok, user, token_record}
else
_ -> nil
end end
end end

View File

@ -337,6 +337,7 @@ defmodule Pleroma.Web.Router do
pipe_through(:pleroma_html) pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe) post("/main/ostatus", UtilController, :remote_subscribe)
get("/main/ostatus", UtilController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteFollowController, :follow) get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow) post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end end
@ -463,6 +464,13 @@ defmodule Pleroma.Web.Router do
get("/birthdays", AccountController, :birthdays) get("/birthdays", AccountController, :birthdays)
end end
scope [] do
pipe_through(:authenticated_api)
get("/settings/:app", SettingsController, :show)
patch("/settings/:app", SettingsController, :update)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend) post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end end
@ -563,6 +571,7 @@ defmodule Pleroma.Web.Router do
get("/bookmarks", StatusController, :bookmarks) get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create) post("/statuses", StatusController, :create)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete) delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog) post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog) post("/statuses/:id/unreblog", StatusController, :unreblog)
@ -622,6 +631,8 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id/card", StatusController, :card) get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by) get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by) get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
get("/statuses/:id/source", StatusController, :show_source)
get("/custom_emojis", CustomEmojiController, :index) get("/custom_emojis", CustomEmojiController, :index)

View File

@ -296,6 +296,24 @@ defp push_to_socket(topic, %Activity{
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
create_activity =
Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
|> Map.put(:object, item.object)
anon_render = StreamerView.render("status_update.json", create_activity)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
send(pid, {:render_with_user, StreamerView, "status_update.json", create_activity})
else
send(pid, {:text, anon_render})
end
end)
end)
end
defp push_to_socket(topic, item) do defp push_to_socket(topic, item) do
anon_render = StreamerView.render("update.json", item) anon_render = StreamerView.render("update.json", item)

View File

@ -0,0 +1,10 @@
<%= if @error do %>
<h2><%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %></h2>
<% else %>
<h2><%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %></h2>
<%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %>
<%= hidden_input f, :status_id, value: @status_id %>
<%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
<%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %>
<% end %>
<% end %>

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger require Logger
alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.Healthcheck alias Pleroma.Healthcheck
@ -16,8 +17,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(Pleroma.Web.ApiSpec.CastAndValidate when action != :remote_subscribe) plug(
plug(Pleroma.Web.Plugs.FederatingPlug when action == :remote_subscribe) Pleroma.Web.ApiSpec.CastAndValidate
when action != :remote_subscribe and action != :show_subscribe_form
)
plug(
Pleroma.Web.Plugs.FederatingPlug
when action == :remote_subscribe
when action == :show_subscribe_form
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
@ -44,7 +53,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do def show_subscribe_form(conn, %{"nickname" => nick}) do
with %User{} = user <- User.get_cached_by_nickname(nick), with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do avatar = User.avatar_url(user) do
conn conn
@ -54,11 +63,52 @@ def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
render(conn, "subscribe.html", %{ render(conn, "subscribe.html", %{
nickname: nick, nickname: nick,
avatar: nil, avatar: nil,
error: "Could not find user" error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - user not found",
"Could not find user"
)
}) })
end end
end end
def show_subscribe_form(conn, %{"status_id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity),
%User{} = user <- User.get_cached_by_ap_id(activity.actor),
avatar = User.avatar_url(user) do
conn
|> render("status_interact.html", %{
status_link: ap_id,
status_id: id,
nickname: user.nickname,
avatar: avatar,
error: false
})
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - status not found",
"Could not find status"
)
})
end
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
show_subscribe_form(conn, %{"nickname" => nick})
end
def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
show_subscribe_form(conn, %{"status_id" => id})
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
@ -69,7 +119,33 @@ def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profil
render(conn, "subscribe.html", %{ render(conn, "subscribe.html", %{
nickname: nick, nickname: nick,
avatar: nil, avatar: nil,
error: "Something went wrong." error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - unknown error",
"Something went wrong."
)
}) })
end end
end end
@ -83,6 +159,15 @@ def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn,
end end
end end
defp get_ap_id(activity) do
object = Pleroma.Object.normalize(activity, fetch: false)
case object do
%{data: %{"id" => ap_id}} -> {:ok, ap_id}
_ -> {:no_ap_id, nil}
end
end
def frontend_configurations(conn, _params) do def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json") render(conn, "frontend_configurations.json")
end end

View File

@ -4,7 +4,9 @@
defmodule Pleroma.Web.TwitterAPI.UtilView do defmodule Pleroma.Web.TwitterAPI.UtilView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML
import Phoenix.HTML.Form import Phoenix.HTML.Form
import Phoenix.HTML.Link
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Gettext alias Pleroma.Web.Gettext

View File

@ -25,6 +25,20 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!() |> Jason.encode!()
end end
def render("status_update.json", %Activity{} = activity, %User{} = user) do
%{
event: "status.update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"show.json",
activity: activity,
for: user
)
|> Jason.encode!()
}
|> Jason.encode!()
end
def render("notification.json", %Notification{} = notify, %User{} = user) do def render("notification.json", %Notification{} = notify, %User{} = user) do
%{ %{
event: "notification", event: "notification",
@ -51,6 +65,19 @@ def render("update.json", %Activity{} = activity) do
|> Jason.encode!() |> Jason.encode!()
end end
def render("status_update.json", %Activity{} = activity) do
%{
event: "status.update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"show.json",
activity: activity
)
|> Jason.encode!()
}
|> Jason.encode!()
end
def render("chat_update.json", %{chat_message_reference: cm_ref}) do def render("chat_update.json", %{chat_message_reference: cm_ref}) do
# Explicitly giving the cmr for the object here, so we don't accidentally # Explicitly giving the cmr for the object here, so we don't accidentally
# send a later 'last_message' that was inserted between inserting this and # send a later 'last_message' that was inserted between inserting this and

View File

@ -9,6 +9,12 @@ defmodule Pleroma.Workers.ReceiverWorker do
@impl Oban.Worker @impl Oban.Worker
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
Federator.perform(:incoming_ap_doc, params) with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
{:ok, res}
else
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
{:error, {:reject, reason}} -> {:cancel, reason}
e -> e
end
end end
end end

View File

@ -80,7 +80,8 @@ def application do
:quack, :quack,
:fast_sanitize, :fast_sanitize,
:os_mon, :os_mon,
:ssl :ssl,
:esshd
], ],
included_applications: [:ex_syslogger] included_applications: [:ex_syslogger]
] ]

View File

@ -3,16 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-15 09:37+0000\n" "POT-Creation-Date: 2020-05-15 09:37+0000\n"
"PO-Revision-Date: 2020-06-02 07:36+0000\n" "PO-Revision-Date: 2022-08-14 11:04+0000\n"
"Last-Translator: Fristi <fristi@subcon.town>\n" "Last-Translator: Fristi <fristi@subcon.town>\n"
"Language-Team: Dutch <https://translate.pleroma.social/projects/pleroma/" "Language-Team: Dutch <http://weblate.pleroma-dev.ebin.club/projects/pleroma/"
"pleroma/nl/>\n" "pleroma-backend-domain-errors/nl/>\n"
"Language: nl\n" "Language: nl\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.0.4\n" "X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file. ## This file is a PO Template file.
## ##
@ -118,7 +118,7 @@ msgstr "Al gestemd"
#: lib/pleroma/web/oauth/oauth_controller.ex:360 #: lib/pleroma/web/oauth/oauth_controller.ex:360
#, elixir-format #, elixir-format
msgid "Bad request" msgid "Bad request"
msgstr "Bad request" msgstr "Ongeldig request"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:425
#, elixir-format #, elixir-format
@ -155,7 +155,7 @@ msgstr "Object kan niet geliked worden"
#: lib/pleroma/web/common_api/utils.ex:556 #: lib/pleroma/web/common_api/utils.ex:556
#, elixir-format #, elixir-format
msgid "Cannot post an empty status without attachments" msgid "Cannot post an empty status without attachments"
msgstr "Status kan niet geplaatst worden zonder tekst of bijlagen" msgstr "Bericht kan niet geplaatst worden zonder tekst of bijlagen"
#: lib/pleroma/web/common_api/utils.ex:504 #: lib/pleroma/web/common_api/utils.ex:504
#, elixir-format #, elixir-format
@ -165,122 +165,122 @@ msgstr "Opmerking dient maximaal %{max_size} karakters te bevatten"
#: lib/pleroma/config/config_db.ex:222 #: lib/pleroma/config/config_db.ex:222
#, elixir-format #, elixir-format
msgid "Config with params %{params} not found" msgid "Config with params %{params} not found"
msgstr "" msgstr "Instelling met parameters %{params} kon niet gevonden worden"
#: lib/pleroma/web/common_api/common_api.ex:95 #: lib/pleroma/web/common_api/common_api.ex:95
#, elixir-format #, elixir-format
msgid "Could not delete" msgid "Could not delete"
msgstr "" msgstr "Verwijderen mislukt"
#: lib/pleroma/web/common_api/common_api.ex:141 #: lib/pleroma/web/common_api/common_api.ex:141
#, elixir-format #, elixir-format
msgid "Could not favorite" msgid "Could not favorite"
msgstr "" msgstr "Favoriet maken mislukt"
#: lib/pleroma/web/common_api/common_api.ex:370 #: lib/pleroma/web/common_api/common_api.ex:370
#, elixir-format #, elixir-format
msgid "Could not pin" msgid "Could not pin"
msgstr "" msgstr "Vastmaken mislukt"
#: lib/pleroma/web/common_api/common_api.ex:112 #: lib/pleroma/web/common_api/common_api.ex:112
#, elixir-format #, elixir-format
msgid "Could not repeat" msgid "Could not repeat"
msgstr "" msgstr "Herhalen mislukt"
#: lib/pleroma/web/common_api/common_api.ex:188 #: lib/pleroma/web/common_api/common_api.ex:188
#, elixir-format #, elixir-format
msgid "Could not unfavorite" msgid "Could not unfavorite"
msgstr "" msgstr "Favoriet ongedaan maken mislukt"
#: lib/pleroma/web/common_api/common_api.ex:380 #: lib/pleroma/web/common_api/common_api.ex:380
#, elixir-format #, elixir-format
msgid "Could not unpin" msgid "Could not unpin"
msgstr "" msgstr "Vastmaken ongedaan maken mislukt"
#: lib/pleroma/web/common_api/common_api.ex:126 #: lib/pleroma/web/common_api/common_api.ex:126
#, elixir-format #, elixir-format
msgid "Could not unrepeat" msgid "Could not unrepeat"
msgstr "" msgstr "Herhalen ongedaan maken mislukt"
#: lib/pleroma/web/common_api/common_api.ex:428 #: lib/pleroma/web/common_api/common_api.ex:428
#: lib/pleroma/web/common_api/common_api.ex:437 #: lib/pleroma/web/common_api/common_api.ex:437
#, elixir-format #, elixir-format
msgid "Could not update state" msgid "Could not update state"
msgstr "" msgstr "Status bijwerken mislukt"
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:202
#, elixir-format #, elixir-format
msgid "Error." msgid "Error."
msgstr "" msgstr "Fout."
#: lib/pleroma/web/twitter_api/twitter_api.ex:106 #: lib/pleroma/web/twitter_api/twitter_api.ex:106
#, elixir-format #, elixir-format
msgid "Invalid CAPTCHA" msgid "Invalid CAPTCHA"
msgstr "" msgstr "Ongeldige CAPTCHA"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:117
#: lib/pleroma/web/oauth/oauth_controller.ex:569 #: lib/pleroma/web/oauth/oauth_controller.ex:569
#, elixir-format #, elixir-format
msgid "Invalid credentials" msgid "Invalid credentials"
msgstr "" msgstr "Ongeldige inloggegevens"
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:38 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:38
#, elixir-format #, elixir-format
msgid "Invalid credentials." msgid "Invalid credentials."
msgstr "" msgstr "Ongeldige inloggegevens."
#: lib/pleroma/web/common_api/common_api.ex:265 #: lib/pleroma/web/common_api/common_api.ex:265
#, elixir-format #, elixir-format
msgid "Invalid indices" msgid "Invalid indices"
msgstr "" msgstr "Ongeldige indexen"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1147 #: lib/pleroma/web/admin_api/admin_api_controller.ex:1147
#, elixir-format #, elixir-format
msgid "Invalid parameters" msgid "Invalid parameters"
msgstr "" msgstr "Ongeldige parameters"
#: lib/pleroma/web/common_api/utils.ex:411 #: lib/pleroma/web/common_api/utils.ex:411
#, elixir-format #, elixir-format
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr "Ongeldig wachtwoord."
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:187
#, elixir-format #, elixir-format
msgid "Invalid request" msgid "Invalid request"
msgstr "" msgstr "Ongeldig request"
#: lib/pleroma/web/twitter_api/twitter_api.ex:109 #: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format #, elixir-format
msgid "Kocaptcha service unavailable" msgid "Kocaptcha service unavailable"
msgstr "" msgstr "Kocaptcha service niet beschikbaar"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:113
#, elixir-format #, elixir-format
msgid "Missing parameters" msgid "Missing parameters"
msgstr "" msgstr "Ontbrekende parameters"
#: lib/pleroma/web/common_api/utils.ex:540 #: lib/pleroma/web/common_api/utils.ex:540
#, elixir-format #, elixir-format
msgid "No such conversation" msgid "No such conversation"
msgstr "" msgstr "Gesprek niet gevonden"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:439 #: lib/pleroma/web/admin_api/admin_api_controller.ex:439
#: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507 #: lib/pleroma/web/admin_api/admin_api_controller.ex:465 lib/pleroma/web/admin_api/admin_api_controller.ex:507
#, elixir-format #, elixir-format
msgid "No such permission_group" msgid "No such permission_group"
msgstr "" msgstr "Permission_group niet gevonden"
#: lib/pleroma/plugs/uploaded_media.ex:74 #: lib/pleroma/plugs/uploaded_media.ex:74
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:485 lib/pleroma/web/admin_api/admin_api_controller.ex:1135
#: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143 #: lib/pleroma/web/feed/user_controller.ex:73 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format #, elixir-format
msgid "Not found" msgid "Not found"
msgstr "" msgstr "Niet gevonden"
#: lib/pleroma/web/common_api/common_api.ex:241 #: lib/pleroma/web/common_api/common_api.ex:241
#, elixir-format #, elixir-format
msgid "Poll's author can't vote" msgid "Poll's author can't vote"
msgstr "" msgstr "De peiling-auteur kan niet stemmen"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20 #: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49 #: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
@ -288,215 +288,215 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:71
#, elixir-format #, elixir-format
msgid "Record not found" msgid "Record not found"
msgstr "" msgstr "Record niet gevonden"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:1153 #: lib/pleroma/web/admin_api/admin_api_controller.ex:1153
#: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32 #: lib/pleroma/web/feed/user_controller.ex:79 lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:32
#: lib/pleroma/web/ostatus/ostatus_controller.ex:149 #: lib/pleroma/web/ostatus/ostatus_controller.ex:149
#, elixir-format #, elixir-format
msgid "Something went wrong" msgid "Something went wrong"
msgstr "" msgstr "Er is iets misgegaan"
#: lib/pleroma/web/common_api/activity_draft.ex:107 #: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format #, elixir-format
msgid "The message visibility must be direct" msgid "The message visibility must be direct"
msgstr "" msgstr "De zichtbaarheid van het bericht dient privé te zijn"
#: lib/pleroma/web/common_api/utils.ex:566 #: lib/pleroma/web/common_api/utils.ex:566
#, elixir-format #, elixir-format
msgid "The status is over the character limit" msgid "The status is over the character limit"
msgstr "" msgstr "Het bericht is langer dan het karakter-limiet"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31 #: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format #, elixir-format
msgid "This resource requires authentication." msgid "This resource requires authentication."
msgstr "" msgstr "Deze gegevens vereisen authenticatie."
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format #, elixir-format
msgid "Throttled" msgid "Throttled"
msgstr "" msgstr "Geremd"
#: lib/pleroma/web/common_api/common_api.ex:266 #: lib/pleroma/web/common_api/common_api.ex:266
#, elixir-format #, elixir-format
msgid "Too many choices" msgid "Too many choices"
msgstr "" msgstr "Teveel keuzes"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:442
#, elixir-format #, elixir-format
msgid "Unhandled activity type" msgid "Unhandled activity type"
msgstr "" msgstr "Niet-ondersteund activiteits-type"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:536 #: lib/pleroma/web/admin_api/admin_api_controller.ex:536
#, elixir-format #, elixir-format
msgid "You can't revoke your own admin status." msgid "You can't revoke your own admin status."
msgstr "" msgstr "Je kan je eigen beheerdersrechten niet intrekken."
#: lib/pleroma/web/oauth/oauth_controller.ex:218 #: lib/pleroma/web/oauth/oauth_controller.ex:218
#: lib/pleroma/web/oauth/oauth_controller.ex:309 #: lib/pleroma/web/oauth/oauth_controller.ex:309
#, elixir-format #, elixir-format
msgid "Your account is currently disabled" msgid "Your account is currently disabled"
msgstr "" msgstr "Je account is momenteel uitgeschakeld"
#: lib/pleroma/web/oauth/oauth_controller.ex:180 #: lib/pleroma/web/oauth/oauth_controller.ex:180
#: lib/pleroma/web/oauth/oauth_controller.ex:332 #: lib/pleroma/web/oauth/oauth_controller.ex:332
#, elixir-format #, elixir-format
msgid "Your login is missing a confirmed e-mail address" msgid "Your login is missing a confirmed e-mail address"
msgstr "" msgstr "Je login bevat geen bevestigd e-mailadres"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:389
#, elixir-format #, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}" msgid "can't read inbox of %{nickname} as %{as_nickname}"
msgstr "" msgstr "kan de inbox van %{nickname} niet lezen als %{as_nickname}"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:472
#, elixir-format #, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}" msgid "can't update outbox of %{nickname} as %{as_nickname}"
msgstr "" msgstr "kan de outbox van %{nickname} niet bijwerken als %{as_nickname}"
#: lib/pleroma/web/common_api/common_api.ex:388 #: lib/pleroma/web/common_api/common_api.ex:388
#, elixir-format #, elixir-format
msgid "conversation is already muted" msgid "conversation is already muted"
msgstr "" msgstr "gesprek is al genegeerd"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:316
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:491
#, elixir-format #, elixir-format
msgid "error" msgid "error"
msgstr "" msgstr "fout"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29 #: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:29
#, elixir-format #, elixir-format
msgid "mascots can only be images" msgid "mascots can only be images"
msgstr "" msgstr "mascottes kunnen alleen afbeeldingen zijn"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:60
#, elixir-format #, elixir-format
msgid "not found" msgid "not found"
msgstr "" msgstr "niet gevonden"
#: lib/pleroma/web/oauth/oauth_controller.ex:395 #: lib/pleroma/web/oauth/oauth_controller.ex:395
#, elixir-format #, elixir-format
msgid "Bad OAuth request." msgid "Bad OAuth request."
msgstr "" msgstr "Ongeldig OAuth request."
#: lib/pleroma/web/twitter_api/twitter_api.ex:115 #: lib/pleroma/web/twitter_api/twitter_api.ex:115
#, elixir-format #, elixir-format
msgid "CAPTCHA already used" msgid "CAPTCHA already used"
msgstr "" msgstr "CAPTCHA is al gebruikt"
#: lib/pleroma/web/twitter_api/twitter_api.ex:112 #: lib/pleroma/web/twitter_api/twitter_api.ex:112
#, elixir-format #, elixir-format
msgid "CAPTCHA expired" msgid "CAPTCHA expired"
msgstr "" msgstr "CAPTCHA is verlopen"
#: lib/pleroma/plugs/uploaded_media.ex:55 #: lib/pleroma/plugs/uploaded_media.ex:55
#, elixir-format #, elixir-format
msgid "Failed" msgid "Failed"
msgstr "" msgstr "Mislukt"
#: lib/pleroma/web/oauth/oauth_controller.ex:411 #: lib/pleroma/web/oauth/oauth_controller.ex:411
#, elixir-format #, elixir-format
msgid "Failed to authenticate: %{message}." msgid "Failed to authenticate: %{message}."
msgstr "" msgstr "Authenticatie mislukt: %{message}."
#: lib/pleroma/web/oauth/oauth_controller.ex:442 #: lib/pleroma/web/oauth/oauth_controller.ex:442
#, elixir-format #, elixir-format
msgid "Failed to set up user account." msgid "Failed to set up user account."
msgstr "" msgstr "Aanmaken van gebruikersaccount is mislukt."
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38 #: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format #, elixir-format
msgid "Insufficient permissions: %{permissions}." msgid "Insufficient permissions: %{permissions}."
msgstr "" msgstr "Niet voldoende rechten: %{permissions}."
#: lib/pleroma/plugs/uploaded_media.ex:94 #: lib/pleroma/plugs/uploaded_media.ex:94
#, elixir-format #, elixir-format
msgid "Internal Error" msgid "Internal Error"
msgstr "" msgstr "Interne Fout"
#: lib/pleroma/web/oauth/fallback_controller.ex:22 #: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29 #: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format #, elixir-format
msgid "Invalid Username/Password" msgid "Invalid Username/Password"
msgstr "" msgstr "Ongeldige Gebruikersnaam/Wachtwoord"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118 #: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format #, elixir-format
msgid "Invalid answer data" msgid "Invalid answer data"
msgstr "" msgstr "Ongeldig antwoord"
#: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128 #: lib/pleroma/web/nodeinfo/nodeinfo_controller.ex:128
#, elixir-format #, elixir-format
msgid "Nodeinfo schema version not handled" msgid "Nodeinfo schema version not handled"
msgstr "" msgstr "Nodeinfo schema wordt niet ondersteund"
#: lib/pleroma/web/oauth/oauth_controller.ex:169 #: lib/pleroma/web/oauth/oauth_controller.ex:169
#, elixir-format #, elixir-format
msgid "This action is outside the authorized scopes" msgid "This action is outside the authorized scopes"
msgstr "" msgstr "Deze actie bevindt zich buiten de gemachtigde scopes"
#: lib/pleroma/web/oauth/fallback_controller.ex:14 #: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format #, elixir-format
msgid "Unknown error, please check the details and try again." msgid "Unknown error, please check the details and try again."
msgstr "" msgstr "Onbekende fout, controleer a.u.b. de details en probeer het opnieuw."
#: lib/pleroma/web/oauth/oauth_controller.ex:116 #: lib/pleroma/web/oauth/oauth_controller.ex:116
#: lib/pleroma/web/oauth/oauth_controller.ex:155 #: lib/pleroma/web/oauth/oauth_controller.ex:155
#, elixir-format #, elixir-format
msgid "Unlisted redirect_uri." msgid "Unlisted redirect_uri."
msgstr "" msgstr "Niet-vermelde redirect_uri."
#: lib/pleroma/web/oauth/oauth_controller.ex:391 #: lib/pleroma/web/oauth/oauth_controller.ex:391
#, elixir-format #, elixir-format
msgid "Unsupported OAuth provider: %{provider}." msgid "Unsupported OAuth provider: %{provider}."
msgstr "" msgstr "Niet ondersteunde OAuth provider: %{provider}."
#: lib/pleroma/uploaders/uploader.ex:72 #: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format #, elixir-format
msgid "Uploader callback timeout" msgid "Uploader callback timeout"
msgstr "" msgstr "Uploader terugkoppeling timeout"
#: lib/pleroma/web/uploader_controller.ex:23 #: lib/pleroma/web/uploader_controller.ex:23
#, elixir-format #, elixir-format
msgid "bad request" msgid "bad request"
msgstr "" msgstr "ongeldig request"
#: lib/pleroma/web/twitter_api/twitter_api.ex:103 #: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format #, elixir-format
msgid "CAPTCHA Error" msgid "CAPTCHA Error"
msgstr "" msgstr "CAPTCHA Fout"
#: lib/pleroma/web/common_api/common_api.ex:200 #: lib/pleroma/web/common_api/common_api.ex:200
#, elixir-format #, elixir-format
msgid "Could not add reaction emoji" msgid "Could not add reaction emoji"
msgstr "" msgstr "Reactie-emoji toevoegen mislukt"
#: lib/pleroma/web/common_api/common_api.ex:211 #: lib/pleroma/web/common_api/common_api.ex:211
#, elixir-format #, elixir-format
msgid "Could not remove reaction emoji" msgid "Could not remove reaction emoji"
msgstr "" msgstr "Reactie-emoji verwijderen mislukt"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129 #: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format #, elixir-format
msgid "Invalid CAPTCHA (Missing parameter: %{name})" msgid "Invalid CAPTCHA (Missing parameter: %{name})"
msgstr "" msgstr "Ongeldige CAPTCHA (Ontbrekende parameter: %{name})"
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92 #: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format #, elixir-format
msgid "List not found" msgid "List not found"
msgstr "" msgstr "Lijst niet gevonden"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124 #: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:124
#, elixir-format #, elixir-format
msgid "Missing parameter: %{name}" msgid "Missing parameter: %{name}"
msgstr "" msgstr "Ontbrekende parameter: %{name}"
#: lib/pleroma/web/oauth/oauth_controller.ex:207 #: lib/pleroma/web/oauth/oauth_controller.ex:207
#: lib/pleroma/web/oauth/oauth_controller.ex:322 #: lib/pleroma/web/oauth/oauth_controller.ex:322
#, elixir-format #, elixir-format
msgid "Password reset is required" msgid "Password reset is required"
msgstr "" msgstr "Wachtwoordherstel is vereist"
#: lib/pleroma/tests/auth_test_controller.ex:9 #: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6 #: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/admin_api_controller.ex:6
@ -528,53 +528,63 @@ msgstr ""
#, elixir-format #, elixir-format
msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped." msgid "Security violation: OAuth scopes check was neither handled nor explicitly skipped."
msgstr "" msgstr ""
"Schending van beveiliging: OAuth scope-controle is niet uitgevoerd en niet "
"expliciet overgeslagen."
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28 #: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format #, elixir-format
msgid "Two-factor authentication enabled, you must use a access token." msgid "Two-factor authentication enabled, you must use a access token."
msgstr "" msgstr ""
"Tweefactor authenticatie is ingeschakeld, een toegangssleutel is verplicht."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:210
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while adding file to pack." msgid "Unexpected error occurred while adding file to pack."
msgstr "" msgstr ""
"Er is een onverwachte fout opgetreden tijdens het toevoegen van het bestand."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:138
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while creating pack." msgid "Unexpected error occurred while creating pack."
msgstr "" msgstr ""
"Er is een onverwachte fout opgetreden tijdens het aanmaken van het pakket."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:278
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while removing file from pack." msgid "Unexpected error occurred while removing file from pack."
msgstr "" msgstr ""
"Er is een onverwachte fout opgetreden tijdens het verwijderen van het "
"bestand."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:250
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while updating file in pack." msgid "Unexpected error occurred while updating file in pack."
msgstr "" msgstr ""
"Er is een onverwachte fout opgetreden tijdens het bijwerken van het bestand."
#: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179 #: lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex:179
#, elixir-format #, elixir-format
msgid "Unexpected error occurred while updating pack metadata." msgid "Unexpected error occurred while updating pack metadata."
msgstr "" msgstr ""
"Er is een onverwachte fout opgetreden tijdens het bijwerken van de pakket-"
"metadata."
#: lib/pleroma/plugs/user_is_admin_plug.ex:21 #: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format #, elixir-format
msgid "User is not an admin." msgid "User is not an admin."
msgstr "" msgstr "Gebruiker is niet een beheerder."
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61 #: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format #, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance" msgid "Web push subscription is disabled on this Pleroma instance"
msgstr "" msgstr "Web push abbonement is uitgeschakeld op deze Pleroma instantie"
#: lib/pleroma/web/admin_api/admin_api_controller.ex:502 #: lib/pleroma/web/admin_api/admin_api_controller.ex:502
#, elixir-format #, elixir-format
msgid "You can't revoke your own admin/moderator status." msgid "You can't revoke your own admin/moderator status."
msgstr "" msgstr "Je kan je eigen beheerders- of moderatorrechten niet intrekken."
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105 #: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:105
#, elixir-format #, elixir-format
msgid "authorization required for timeline view" msgid "authorization required for timeline view"
msgstr "" msgstr "machtiging is vereist voor de tijdlijn weergave"

View File

@ -0,0 +1,165 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-08-13 13:32+0300\n"
"PO-Revision-Date: 2022-08-14 11:04+0000\n"
"Last-Translator: Fristi <fristi@subcon.town>\n"
"Language-Team: Dutch <http://weblate.pleroma-dev.ebin.club/projects/pleroma/"
"pleroma-backend-domain-posix_errors/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
msgid "eperm"
msgstr "Uitvoering niet toegestaan"
msgid "eacces"
msgstr "Toegang geweigerd"
msgid "eagain"
msgstr "Resource tijdelijk niet beschikbaar"
msgid "ebadf"
msgstr "Ongeldige file descriptor"
msgid "ebadmsg"
msgstr "Ongeldig bericht"
msgid "ebusy"
msgstr "Apparaat of resource bezet"
msgid "edeadlk"
msgstr "Resource deadlock vermeden"
msgid "edeadlock"
msgstr "Resource deadlock vermeden"
msgid "edquot"
msgstr "Schijf-quota overschreden"
msgid "eexist"
msgstr "Bestand bestaat"
msgid "efault"
msgstr "Ongeldig adres"
msgid "efbig"
msgstr "Bestand is te groot"
msgid "eftype"
msgstr "Ongepast bestands-type of formaat"
msgid "eintr"
msgstr "Onderbroken systeem aanroep"
msgid "einval"
msgstr "Ongeldig argument"
msgid "eio"
msgstr "Input/output fout"
msgid "eisdir"
msgstr "Illegale bewerking op een directory"
msgid "eloop"
msgstr "Te veel niveau's van symbolische koppelingen"
msgid "emfile"
msgstr "Te veel geopende bestanden"
msgid "emlink"
msgstr "Te veel koppelingen"
msgid "emultihop"
msgstr "Multihop geprobeerd"
msgid "enametoolong"
msgstr "Bestandsnaam is te lang"
msgid "enfile"
msgstr "Te veel geopende bestanden in systeem"
msgid "enobufs"
msgstr "Geen buffer-ruimte beschikbaar"
msgid "enodev"
msgstr "Apparaat bestaat niet"
msgid "enolck"
msgstr "Geen sloten beschikbaar"
msgid "enolink"
msgstr "Koppeling is ongedaan gemaakt"
msgid "enoent"
msgstr "Bestand of directory bestaat niet"
msgid "enomem"
msgstr "Geheugen kon niet toegewezen worden"
msgid "enospc"
msgstr "Geen ruimte over op apparaat"
msgid "enosr"
msgstr ""
msgid "enostr"
msgstr ""
msgid "enosys"
msgstr ""
msgid "enotblk"
msgstr ""
msgid "enotdir"
msgstr ""
msgid "enotsup"
msgstr ""
msgid "enxio"
msgstr ""
msgid "eopnotsupp"
msgstr ""
msgid "eoverflow"
msgstr ""
msgid "epipe"
msgstr ""
msgid "erange"
msgstr ""
msgid "erofs"
msgstr ""
msgid "espipe"
msgstr ""
msgid "esrch"
msgstr ""
msgid "estale"
msgstr ""
msgid "etxtbsy"
msgstr ""
msgid "exdev"
msgstr ""

View File

@ -0,0 +1,567 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-08-13 13:24+0300\n"
"PO-Revision-Date: 2022-08-14 11:04+0000\n"
"Last-Translator: Fristi <fristi@subcon.town>\n"
"Language-Team: Dutch <http://weblate.pleroma-dev.ebin.club/projects/pleroma/"
"pleroma-backend-domain-static_pages/nl/>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here as no
## effect: edit them in PO (.po) files instead.
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button"
msgid "Authorize"
msgstr "Machtigen"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error fetching user"
msgstr "Fout bij ophalen gebruiker"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header"
msgid "Remote follow"
msgstr "Extern volgen"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "placeholder text for auth code entry"
msgid "Authentication code"
msgstr "Authenticatiecode"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
#, elixir-autogen, elixir-format
msgctxt "placeholder text for password entry"
msgid "Password"
msgstr "Wachtwoord"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "placeholder text for username entry"
msgid "Username"
msgstr "Gebruikersnaam"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for login"
msgid "Authorize"
msgstr "Machtigen"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for mfa"
msgid "Authorize"
msgstr "Machtigen"
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error following account"
msgstr "Fout bij volgen van account"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header, need login"
msgid "Log in to follow"
msgstr "Log in om te volgen"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow mfa header"
msgid "Two-factor authentication"
msgstr "Tweefactor authenticatie"
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow success"
msgid "Account followed!"
msgstr "Account gevolgd!"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
#, elixir-autogen, elixir-format
msgctxt "placeholder text for account id"
msgid "Your account ID, e.g. lain@quitter.se"
msgstr "Je account ID, b.v. gebruiker@instantie.net"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for following with a remote account"
msgid "Follow"
msgstr "Volgen"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error: %{error}"
msgstr "Fout: %{error}"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header"
msgid "Remotely follow %{nickname}"
msgstr "%{nickname} extern volgen"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "password reset button"
msgid "Reset"
msgstr "Herstellen"
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "password reset failed homepage link"
msgid "Homepage"
msgstr "Homepagina"
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset failed message"
msgid "Password reset failed"
msgstr "Wachtwoordherstel mislukt"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "password reset form confirm password prompt"
msgid "Confirmation"
msgstr "Bevestiging"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "password reset form password prompt"
msgid "Password"
msgstr "Wachtwoord"
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset invalid token message"
msgid "Invalid Token"
msgstr "Ongeldige Token"
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "password reset successful homepage link"
msgid "Homepage"
msgstr "Homepagina"
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset successful message"
msgid "Password changed!"
msgstr "Wachtwoord gewijzigd!"
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
#, elixir-autogen, elixir-format
msgctxt "tag feed description"
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
msgstr ""
"Dit zijn openbare berichten die getagd zijn met #%{tag}. Je kunt op deze "
"reageren indien je een account hebt in de fediverse."
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "oauth authorization exists page title"
msgid "Authorization exists"
msgstr "Machtiging bestaat"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:32
#, elixir-autogen, elixir-format
msgctxt "oauth authorize approve button"
msgid "Approve"
msgstr "Goedkeuren"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:30
#, elixir-autogen, elixir-format
msgctxt "oauth authorize cancel button"
msgid "Cancel"
msgstr "Annuleren"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:23
#, elixir-autogen, elixir-format
msgctxt "oauth authorize message"
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
msgstr ""
"Applicatie <strong>%{client_name}</strong> vraagt om toegang tot je account."
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "oauth authorized page title"
msgid "Successfully authorized"
msgstr "Machtiging is geslaagd"
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "oauth external provider page title"
msgid "Sign in with external provider"
msgstr "Inloggen bij externe provider"
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
#, elixir-autogen, elixir-format
msgctxt "oauth external provider sign in button"
msgid "Sign in with %{strategy}"
msgstr "Inloggen met %{strategy}"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:54
#, elixir-autogen, elixir-format
msgctxt "oauth login button"
msgid "Log In"
msgstr "Inloggen"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:51
#, elixir-autogen, elixir-format
msgctxt "oauth login password prompt"
msgid "Password"
msgstr "Wachtwoord"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:47
#, elixir-autogen, elixir-format
msgctxt "oauth login username prompt"
msgid "Username"
msgstr "Gebruikersnaam"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:39
#, elixir-autogen, elixir-format
msgctxt "oauth register nickname prompt"
msgid "Pleroma Handle"
msgstr "Pleroma Gebruiker"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
#, elixir-autogen, elixir-format
msgctxt "oauth register nickname unchangeable warning"
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
msgstr ""
"Let op! Je kunt je accountnaam hierna niet meer wijzigen. Je kunt echter wel "
"nog je weergavenaam wijzigen."
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
#, elixir-autogen, elixir-format
msgctxt "oauth register page email prompt"
msgid "Email"
msgstr "E-mail"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
#, elixir-autogen, elixir-format
msgctxt "oauth register page fill form prompt"
msgid "If you'd like to register a new account, please provide the details below."
msgstr ""
"Indien je graag een nieuw account wilt registreren, vul dan a.u.b de "
"onderstaande details in."
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
#, elixir-autogen, elixir-format
msgctxt "oauth register page login button"
msgid "Proceed as existing user"
msgstr "Doorgaan als bestaande gebruiker"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
#, elixir-autogen, elixir-format
msgctxt "oauth register page login password prompt"
msgid "Password"
msgstr "Wachtwoord"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
#, elixir-autogen, elixir-format
msgctxt "oauth register page login prompt"
msgid "Alternatively, sign in to connect to existing account."
msgstr "Alternatief, log in om te verbinden met een bestaand account."
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
#, elixir-autogen, elixir-format
msgctxt "oauth register page login username prompt"
msgid "Name or email"
msgstr "Naam of e-mail"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
#, elixir-autogen, elixir-format
msgctxt "oauth register page nickname prompt"
msgid "Nickname"
msgstr "Weergavenaam"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
#, elixir-autogen, elixir-format
msgctxt "oauth register page register button"
msgid "Proceed as new user"
msgstr "Doorgaan als nieuwe gebruiker"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "oauth register page title"
msgid "Registration Details"
msgstr "Registratiegegevens"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:36
#, elixir-autogen, elixir-format
msgctxt "oauth register page title"
msgid "This is the first time you visit! Please enter your Pleroma handle."
msgstr "Dit is je eerste bezoek! Vul a.u.b. je Pleroma gebruikersnaam in."
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "oauth scopes message"
msgid "The following permissions will be granted"
msgstr "De volgende rechten zullen worden toegekend"
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:2
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "oauth token code message"
msgid "Token code is <br>%{token}"
msgstr "Token code is <br>%{token}"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "mfa auth code prompt"
msgid "Authentication code"
msgstr "Authenticatiecode"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "mfa auth page title"
msgid "Two-factor authentication"
msgstr "Tweefactor authenticatie"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:23
#, elixir-autogen, elixir-format
msgctxt "mfa auth page use recovery code link"
msgid "Enter a two-factor recovery code"
msgstr "Voer een tweefactor herstelcode in"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:20
#, elixir-autogen, elixir-format
msgctxt "mfa auth verify code button"
msgid "Verify"
msgstr "Controleren"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "mfa recover page title"
msgid "Two-factor recovery"
msgstr "Tweefactor herstel"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "mfa recover recovery code prompt"
msgid "Recovery code"
msgstr "Herstelcode"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:23
#, elixir-autogen, elixir-format
msgctxt "mfa recover use 2fa code link"
msgid "Enter a two-factor code"
msgstr "Voer een tweefactor code in"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:20
#, elixir-autogen, elixir-format
msgctxt "mfa recover verify recovery code button"
msgid "Verify"
msgstr "Controleren"
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "static fe profile page remote follow button"
msgid "Remote follow"
msgstr "Extern volgen"
#: lib/pleroma/web/templates/email/digest.html.eex:163
#, elixir-autogen, elixir-format
msgctxt "digest email header line"
msgid "Hey %{nickname}, here is what you've missed!"
msgstr "Hoi %{nickname}, dit is wat je hebt gemist!"
#: lib/pleroma/web/templates/email/digest.html.eex:544
#, elixir-autogen, elixir-format
msgctxt "digest email receiver address"
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
msgstr ""
"Het e-mailadres waarmee je bent ingeschreven is <a href='mailto:%{@user."
"email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
#: lib/pleroma/web/templates/email/digest.html.eex:538
#, elixir-autogen, elixir-format
msgctxt "digest email sending reason"
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
msgstr ""
"Je ontvangt deze e-mail omdat je bent ingeschreven voor overzichts-mails te "
"ontvangen van <b>%{instance}</b> Pleroma instantie."
#: lib/pleroma/web/templates/email/digest.html.eex:547
#, elixir-autogen, elixir-format
msgctxt "digest email unsubscribe action"
msgid "To unsubscribe, please go %{here}."
msgstr "Je kunt je %{here} uitschrijven voor deze e-mails."
#: lib/pleroma/web/templates/email/digest.html.eex:547
#, elixir-autogen, elixir-format
msgctxt "digest email unsubscribe action link text"
msgid "here"
msgstr "hier"
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "mailer unsubscribe failed message"
msgid "UNSUBSCRIBE FAILURE"
msgstr "UITSCHRIJVEN MISLUKT"
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "mailer unsubscribe successful message"
msgid "UNSUBSCRIBE SUCCESSFUL"
msgstr "UITSCHRIJVEN GESLAAGD"
#: lib/pleroma/web/templates/email/digest.html.eex:385
#, elixir-format
msgctxt "new followers count header"
msgid "%{count} New Follower"
msgid_plural "%{count} New Followers"
msgstr[0] "%{count} Nieuwe Volger"
msgstr[1] "%{count} Nieuwe Volgers"
#: lib/pleroma/emails/user_email.ex:356
#, elixir-autogen, elixir-format
msgctxt "account archive email body - self-requested"
msgid "<p>You requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
"<p>Je hebt een verzoek ingediend voor een volledige back-up van je Pleroma "
"account. Deze is gereed om te downloaden:</p>\n"
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
#: lib/pleroma/emails/user_email.ex:384
#, elixir-autogen, elixir-format
msgctxt "account archive email subject"
msgid "Your account archive is ready"
msgstr "Je account archief is gereed"
#: lib/pleroma/emails/user_email.ex:188
#, elixir-autogen, elixir-format
msgctxt "approval pending email body"
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
msgstr ""
"<h3>Goedkeuring in afwachting</h3>\n"
"<p>Je account bij %{instance_name} zal worden beoordeeld door de beheerders. "
"Je zult een opvolgende e-mail ontvangen wanneer je account goed gekeurd "
"is.</p>\n"
#: lib/pleroma/emails/user_email.ex:202
#, elixir-autogen, elixir-format
msgctxt "approval pending email subject"
msgid "Your account is awaiting approval"
msgstr "Je account is in afwachting van goedkeuring"
#: lib/pleroma/emails/user_email.ex:158
#, elixir-autogen, elixir-format
msgctxt "confirmation email body"
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
msgstr ""
"<h3>Bedankt voor het registreren bij %{instance_name}</h3>\n"
"<p>Bevestiging via e-mail is vereist om je account te activeren.</p>\n"
"<p>Je kunt je account activeren door op <a href=\"%{confirmation_url}\">deze "
"link te klikken</a>.</p>\n"
#: lib/pleroma/emails/user_email.ex:174
#, elixir-autogen, elixir-format
msgctxt "confirmation email subject"
msgid "%{instance_name} account confirmation"
msgstr "%{instance_name} account bevestiging"
#: lib/pleroma/emails/user_email.ex:310
#, elixir-autogen, elixir-format
msgctxt "digest email subject"
msgid "Your digest from %{instance_name}"
msgstr "Je overzicht van %{instance_name}"
#: lib/pleroma/emails/user_email.ex:81
#, elixir-autogen, elixir-format
msgctxt "password reset email body"
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
msgstr ""
"<h3>Herstel je wachtwoord bij %{instance_name}</h3>\n"
"<p>Iemand heeft een verzoek ingediend om het wachtwoord van je account bij "
"%{instance_name} te herstellen.</p>\n"
"<p>Als je dit zelf geweest bent, volg dan de volgende link om door te gaan: "
"<a href=\"%{password_reset_url}\">wachtwoord herstellen</a>.</p>\n"
"<p>Indien je dit niet geweest bent, hoef je geen verdere acties te "
"ondernemen: je gegevens zijn veilig en je wachtwoord is niet gewijzigd.</p>\n"
#: lib/pleroma/emails/user_email.ex:98
#, elixir-autogen, elixir-format
msgctxt "password reset email subject"
msgid "Password reset"
msgstr "Wachtwoord herstellen"
#: lib/pleroma/emails/user_email.ex:215
#, elixir-autogen, elixir-format
msgctxt "successful registration email body"
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
msgstr ""
"<h3>Hoi @%{nickname},</h3>\n"
"<p>Het registreren van je account bij %{instance_name} is gelukt.</p>\n"
"<p>Er zijn geen verdere stappen vereist om je account te activeren.</p>\n"
#: lib/pleroma/emails/user_email.ex:231
#, elixir-autogen, elixir-format
msgctxt "successful registration email subject"
msgid "Account registered on %{instance_name}"
msgstr "Account registratie bij %{instance_name}"
#: lib/pleroma/emails/user_email.ex:119
#, elixir-autogen, elixir-format
msgctxt "user invitation email body"
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Pleroma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
msgstr ""
"<h3>Je bent uitgenodigd bij %{instance_name}</h3>\n"
"<p>%{inviter_name} nodigt je uit om je te registreren bij %{instance_name}, "
"een instantie van het Pleroma gefedereerde sociale netwerk.</p>\n"
"<p>Om je te registreren, klink op de volgende link: <a href=\""
"%{registration_url}\">uitnodiging accepteren</a>.</p>\n"
#: lib/pleroma/emails/user_email.ex:136
#, elixir-autogen, elixir-format
msgctxt "user invitation email subject"
msgid "Invitation to %{instance_name}"
msgstr "Uitnodiging van %{instance_name}"
#: lib/pleroma/emails/user_email.ex:53
#, elixir-autogen, elixir-format
msgctxt "welcome email html body"
msgid "Welcome to %{instance_name}!"
msgstr "Welkom bij %{instance_name}!"
#: lib/pleroma/emails/user_email.ex:41
#, elixir-autogen, elixir-format
msgctxt "welcome email subject"
msgid "Welcome to %{instance_name}!"
msgstr "Welkom bij %{instance_name}!"
#: lib/pleroma/emails/user_email.ex:65
#, elixir-autogen, elixir-format
msgctxt "welcome email text body"
msgid "Welcome to %{instance_name}!"
msgstr "Welkom bij %{instance_name}!"
#: lib/pleroma/emails/user_email.ex:368
#, elixir-autogen, elixir-format
msgctxt "account archive email body - admin requested"
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Pleroma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
"<p>Beheerder @%{admin_nickname} heeft een verzoek ingediend voor een "
"volledige back-up van je Pleroma account. Deze is gereed om te "
"downloaden:</p>\n"
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"

View File

@ -0,0 +1,51 @@
defmodule Pleroma.Repo.Migrations.AddUpdateToNotificationsEnum do
use Ecto.Migration
@disable_ddl_transaction true
def up do
"""
alter type notification_type add value 'update'
"""
|> execute()
end
# 20210717000000_add_poll_to_notifications_enum.exs
def down do
alter table(:notifications) do
modify(:type, :string)
end
"""
delete from notifications where type = 'update'
"""
|> execute()
"""
drop type if exists notification_type
"""
|> execute()
"""
create type notification_type as enum (
'follow',
'follow_request',
'mention',
'move',
'pleroma:emoji_reaction',
'pleroma:chat_mention',
'reblog',
'favourite',
'pleroma:report',
'poll'
)
"""
|> execute()
"""
alter table notifications
alter column type type notification_type using (type::notification_type)
"""
|> execute()
end
end

View File

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.AddAssociatedObjectIdFunction do
use Ecto.Migration
def up do
statement = """
CREATE OR REPLACE FUNCTION associated_object_id(data jsonb) RETURNS varchar AS $$
DECLARE
object_data jsonb;
BEGIN
IF jsonb_typeof(data->'object') = 'array' THEN
object_data := data->'object'->0;
ELSE
object_data := data->'object';
END IF;
IF jsonb_typeof(object_data->'id') = 'string' THEN
RETURN object_data->>'id';
ELSIF jsonb_typeof(object_data) = 'string' THEN
RETURN object_data#>>'{}';
ELSE
RETURN NULL;
END IF;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
"""
execute(statement)
end
def down do
execute("DROP FUNCTION IF EXISTS associated_object_id(data jsonb)")
end
end

View File

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.SwitchToAssociatedObjectIdIndex do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up do
drop_if_exists(
index(:activities, ["(coalesce(data->'object'->>'id', data->>'object'))"],
name: :activities_create_objects_index
)
)
create(
index(:activities, ["associated_object_id(data)"],
name: :activities_create_objects_index,
concurrently: true
)
)
end
def down do
drop_if_exists(
index(:activities, ["associated_object_id(data)"], name: :activities_create_objects_index)
)
create(
index(:activities, ["(coalesce(data->'object'->>'id', data->>'object'))"],
name: :activities_create_objects_index,
concurrently: true
)
)
end
end

View File

@ -0,0 +1,18 @@
defmodule Pleroma.Repo.Migrations.DataMigrationDeleteContextObjects do
use Ecto.Migration
require Logger
def up do
dt = NaiveDateTime.utc_now()
execute(
"INSERT INTO data_migrations(name, inserted_at, updated_at) " <>
"VALUES ('delete_context_objects', '#{dt}', '#{dt}') ON CONFLICT DO NOTHING;"
)
end
def down do
execute("DELETE FROM data_migrations WHERE name = 'delete_context_objects';")
end
end

View File

@ -0,0 +1,156 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.ChangeThreadVisibilityToUseNewObjectIdIndex do
use Ecto.Migration
def up do
execute(update_thread_visibility())
end
def down do
execute(restore_thread_visibility())
end
def update_thread_visibility do
"""
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar, local_public varchar default '') RETURNS boolean AS $$
DECLARE
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
child objects%ROWTYPE;
activity activities%ROWTYPE;
author_fa varchar;
valid_recipients varchar[];
actor_user_following varchar[];
BEGIN
--- Fetch actor following
SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships
JOIN users ON users.id = following_relationships.follower_id
JOIN users AS following ON following.id = following_relationships.following_id
WHERE users.ap_id = actor;
--- Fetch our initial activity.
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
LOOP
--- Ensure that we have an activity before continuing.
--- If we don't, the thread is not satisfiable.
IF activity IS NULL THEN
RETURN false;
END IF;
--- We only care about Create activities.
IF activity.data->>'type' != 'Create' THEN
RETURN true;
END IF;
--- Normalize the child object into child.
SELECT * INTO child FROM objects
INNER JOIN activities ON associated_object_id(activities.data) = objects.data->>'id'
WHERE associated_object_id(activity.data) = objects.data->>'id';
--- Fetch the author's AS2 following collection.
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
--- Prepare valid recipients array.
valid_recipients := ARRAY[actor, public];
--- If we specified local public, add it.
IF local_public <> '' THEN
valid_recipients := valid_recipients || local_public;
END IF;
IF ARRAY[author_fa] && actor_user_following THEN
valid_recipients := valid_recipients || author_fa;
END IF;
--- Check visibility.
IF NOT valid_recipients && activity.recipients THEN
--- activity not visible, break out of the loop
RETURN false;
END IF;
--- If there's a parent, load it and do this all over again.
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
SELECT * INTO activity FROM activities
INNER JOIN objects ON associated_object_id(activities.data) = objects.data->>'id'
WHERE child.data->>'inReplyTo' = objects.data->>'id';
ELSE
RETURN true;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
"""
end
# priv/repo/migrations/20220509180452_change_thread_visibility_to_be_local_only_aware.exs
def restore_thread_visibility do
"""
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar, local_public varchar default '') RETURNS boolean AS $$
DECLARE
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
child objects%ROWTYPE;
activity activities%ROWTYPE;
author_fa varchar;
valid_recipients varchar[];
actor_user_following varchar[];
BEGIN
--- Fetch actor following
SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships
JOIN users ON users.id = following_relationships.follower_id
JOIN users AS following ON following.id = following_relationships.following_id
WHERE users.ap_id = actor;
--- Fetch our initial activity.
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
LOOP
--- Ensure that we have an activity before continuing.
--- If we don't, the thread is not satisfiable.
IF activity IS NULL THEN
RETURN false;
END IF;
--- We only care about Create activities.
IF activity.data->>'type' != 'Create' THEN
RETURN true;
END IF;
--- Normalize the child object into child.
SELECT * INTO child FROM objects
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
--- Fetch the author's AS2 following collection.
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
--- Prepare valid recipients array.
valid_recipients := ARRAY[actor, public];
--- If we specified local public, add it.
IF local_public <> '' THEN
valid_recipients := valid_recipients || local_public;
END IF;
IF ARRAY[author_fa] && actor_user_following THEN
valid_recipients := valid_recipients || author_fa;
END IF;
--- Check visibility.
IF NOT valid_recipients && activity.recipients THEN
--- activity not visible, break out of the loop
RETURN false;
END IF;
--- If there's a parent, load it and do this all over again.
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
SELECT * INTO activity FROM activities
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE child.data->>'inReplyTo' = objects.data->>'id';
ELSE
RETURN true;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
"""
end
end

Binary file not shown.

View File

@ -36,7 +36,8 @@
"@id": "as:alsoKnownAs", "@id": "as:alsoKnownAs",
"@type": "@id" "@type": "@id"
}, },
"vcard": "http://www.w3.org/2006/vcard/ns#" "vcard": "http://www.w3.org/2006/vcard/ns#",
"formerRepresentations": "litepub:formerRepresentations"
} }
] ]
} }

View File

@ -61,6 +61,12 @@ def handle_cast(:refresh, _state) do
{:noreply, @init_state} {:noreply, @init_state}
end end
# Don't actually restart during tests.
# We just check if the correct call has been done.
# If we actually restart, we get errors during the tests like
# (RuntimeError) could not lookup Ecto repo Pleroma.Repo because it was not started or
# it does not exist
# See tests in Pleroma.Config.TransferTaskTest
def handle_cast({:restart, :test, _}, state) do def handle_cast({:restart, :test, _}, state) do
Logger.debug("pleroma manually restarted") Logger.debug("pleroma manually restarted")
{:noreply, Map.put(state, :need_reboot, false)} {:noreply, Map.put(state, :need_reboot, false)}
@ -74,6 +80,12 @@ def handle_cast({:restart, _, delay}, state) do
def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state} def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state}
# Don't actually restart during tests.
# We just check if the correct call has been done.
# If we actually restart, we get errors during the tests like
# (RuntimeError) could not lookup Ecto repo Pleroma.Repo because it was not started or
# it does not exist
# See tests in Pleroma.Config.TransferTaskTest
def handle_cast({:after_boot, :test}, state) do def handle_cast({:after_boot, :test}, state) do
Logger.debug("pleroma restarted after boot") Logger.debug("pleroma restarted after boot")
state = %{state | after_boot: true, rebooted: true} state = %{state | after_boot: true, rebooted: true}

View File

@ -13,7 +13,8 @@ def project do
def application do def application do
[ [
mod: {Restarter, []} mod: {Restarter, []},
extra_applications: [:logger]
] ]
end end

View File

@ -0,0 +1,61 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://p.helene.moe/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"actor": "https://p.helene.moe/users/helene",
"attachment": [],
"attributedTo": "https://p.helene.moe/users/helene",
"cc": [
"https://p.helene.moe/users/helene/followers"
],
"context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"directMessage": false,
"id": "https://p.helene.moe/activities/5f80db86-a9bb-4883-9845-fbdbd1478f3a",
"object": {
"actor": "https://p.helene.moe/users/helene",
"attachment": [],
"attributedTo": "https://p.helene.moe/users/helene",
"cc": [
"https://p.helene.moe/users/helene/followers"
],
"content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AHntpQ4T3J4OSnpgMC\" href=\"https://mk.absturztau.be/@mametsuko\" rel=\"ugc\">@<span>mametsuko</span></a></span> meow",
"context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"id": "https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4",
"inReplyTo": "https://mk.absturztau.be/notes/93e7nm8wqg",
"published": "2022-08-02T13:46:58.403996Z",
"sensitive": null,
"source": "@mametsuko@mk.absturztau.be meow",
"summary": "",
"tag": [
{
"href": "https://mk.absturztau.be/users/8ozbzjs3o8",
"name": "@mametsuko@mk.absturztau.be",
"type": "Mention"
}
],
"to": [
"https://mk.absturztau.be/users/8ozbzjs3o8",
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
},
"published": "2022-08-02T13:46:58.403883Z",
"tag": [
{
"href": "https://mk.absturztau.be/users/8ozbzjs3o8",
"name": "@mametsuko@mk.absturztau.be",
"type": "Mention"
}
],
"to": [
"https://mk.absturztau.be/users/8ozbzjs3o8",
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Create"
}

View File

@ -0,0 +1,50 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://p.helene.moe/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"alsoKnownAs": [],
"attachment": [
{
"name": "Timezone",
"type": "PropertyValue",
"value": "UTC+2 (Paris/Berlin)"
}
],
"capabilities": {
"acceptsChatMessages": true
},
"discoverable": true,
"endpoints": {
"oauthAuthorizationEndpoint": "https://p.helene.moe/oauth/authorize",
"oauthRegistrationEndpoint": "https://p.helene.moe/api/v1/apps",
"oauthTokenEndpoint": "https://p.helene.moe/oauth/token",
"sharedInbox": "https://p.helene.moe/inbox",
"uploadMedia": "https://p.helene.moe/api/ap/upload_media"
},
"featured": "https://p.helene.moe/users/helene/collections/featured",
"followers": "https://p.helene.moe/users/helene/followers",
"following": "https://p.helene.moe/users/helene/following",
"icon": {
"type": "Image",
"url": "https://p.helene.moe/media/9a39209daa5a66b7ebb0547b08bf8360aa9d8d65a4ffba2603c6ffbe6aecb432.jpg"
},
"id": "https://p.helene.moe/users/helene",
"inbox": "https://p.helene.moe/users/helene/inbox",
"manuallyApprovesFollowers": false,
"name": "Hélène",
"outbox": "https://p.helene.moe/users/helene/outbox",
"preferredUsername": "helene",
"publicKey": {
"id": "https://p.helene.moe/users/helene#main-key",
"owner": "https://p.helene.moe/users/helene",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtoSBPU/VS2Kx3f6ap3zv\nZVacJsgUfaoFb3c2ii/FRh9RmRVlarq8sJXcjsQt1e0oxWaWJaIDDwyKZPt6hXae\nrY/AiGGeNu+NA+BtY7l7+9Yu67HUyT62+1qAwYHKBXX3fLOPs/YmQI0Tt0c4wKAG\nKEkiYsRizghgpzUC6jqdKV71DJkUZ8yhckCGb2fLko1ajbWEssdaP51aLsyRMyC2\nuzeWrxtD4O/HG0ea4S6y5X6hnsAHIK4Y3nnyIQ6pn4tOsl3HgqkjXE9MmZSvMCFx\nBq89TfZrVXNa2gSZdZLdbbJstzEScQWNt1p6tA6rM+e4JXYGr+rMdF3G+jV7afI2\nFQIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"summary": "I can speak: Français, English, Deutsch (nicht sehr gut), 日本語 (not very well)",
"tag": [],
"type": "Person",
"url": "https://p.helene.moe/users/helene"
}

View File

@ -0,0 +1,65 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"Hashtag": "as:Hashtag",
"quoteUrl": "as:quoteUrl",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"featured": "toot:featured",
"discoverable": "toot:discoverable",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"misskey": "https://misskey-hub.net/ns#",
"_misskey_content": "misskey:_misskey_content",
"_misskey_quote": "misskey:_misskey_quote",
"_misskey_reaction": "misskey:_misskey_reaction",
"_misskey_votes": "misskey:_misskey_votes",
"_misskey_talk": "misskey:_misskey_talk",
"isCat": "misskey:isCat",
"vcard": "http://www.w3.org/2006/vcard/ns#"
}
],
"type": "Person",
"id": "https://mk.absturztau.be/users/8ozbzjs3o8",
"inbox": "https://mk.absturztau.be/users/8ozbzjs3o8/inbox",
"outbox": "https://mk.absturztau.be/users/8ozbzjs3o8/outbox",
"followers": "https://mk.absturztau.be/users/8ozbzjs3o8/followers",
"following": "https://mk.absturztau.be/users/8ozbzjs3o8/following",
"featured": "https://mk.absturztau.be/users/8ozbzjs3o8/collections/featured",
"sharedInbox": "https://mk.absturztau.be/inbox",
"endpoints": {
"sharedInbox": "https://mk.absturztau.be/inbox"
},
"url": "https://mk.absturztau.be/@mametsuko",
"preferredUsername": "mametsuko",
"name": "mametschko",
"summary": "<p><span>nya, ich bin eine Brotperson</span></p>",
"icon": {
"type": "Image",
"url": "https://mk.absturztau.be/files/webpublic-3b5594f4-fa52-4548-b4e3-c379ae2143ed",
"sensitive": false,
"name": null
},
"image": {
"type": "Image",
"url": "https://mk.absturztau.be/files/webpublic-0d03b03d-b14b-4916-ac3d-8a137118ec84",
"sensitive": false,
"name": null
},
"tag": [],
"manuallyApprovesFollowers": true,
"discoverable": false,
"publicKey": {
"id": "https://mk.absturztau.be/users/8ozbzjs3o8#main-key",
"type": "Key",
"owner": "https://mk.absturztau.be/users/8ozbzjs3o8",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuN/S1spBGmh8FXI1Bt16\nXB7Cc0QutBp7UPgmDNHjOfsq0zrF4g3L1UBxvrpU0XX77XPMCd9yPvGwAYURH2mv\ntIcYuE+R90VLDmBu5MTVthcG2D874eCZ2rD2YsEYmN5AjTX7QBIqCck+qDhVWkkM\nEZ6S5Ht6IJ5Of74eKffXElQI/C6QB+9uEDOmPk0jCzgI5gw7xvJqFj/DIF4kUUAu\nA89JqaFZzZlkrSrj4cr48bLN/YOmpdaHu0BKHaDSHct4+MqlixqovgdB6RboCEDw\ne4Aeav7+Q0Y9oGIvuggg0Q+nCubnVNnaPyzd817tpPVzyZmTts+DKyDuv90SX3nR\nsPaNa5Ty60eqplUk4b7X1gSvuzBJUFBxTVV84WnjwoeoydaS6rSyjCDPGLBjaByc\nFyWMMEb/zlQyhLZfBlvT7k96wRSsMszh2hDALWmgYIhq/jNwINvALJ1GKLNHHKZ4\nyz2LnxVpRm2rWrZzbvtcnSQOt3LaPSZn8Wgwv4buyHF02iuVuIamZVtKexsE1Ixl\nIi9qa3AKEc5gOzYXhRhvHaruzoCehUbb/UHC5c8Tto8L5G1xYzjLP3qj3PT9w/wM\n+k1Ra/4JhuAnVFROOoOmx9rIELLHH7juY2nhM7plGhyt1M5gysgqEloij8QzyQU2\nZK1YlAERG2XFO6br8omhcmECAwEAAQ==\n-----END PUBLIC KEY-----\n"
},
"isCat": true,
"vcard:Address": "Vienna, Austria"
}

View File

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey-hub.net/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"id":"https://mk.absturztau.be/notes/93e7nm8wqg/activity","actor":"https://mk.absturztau.be/users/8ozbzjs3o8","type":"Create","published":"2022-08-01T11:06:49.568Z","object":{"id":"https://mk.absturztau.be/notes/93e7nm8wqg","type":"Note","attributedTo":"https://mk.absturztau.be/users/8ozbzjs3o8","summary":null,"content":"<p><span>meow</span></p>","_misskey_content":"meow","published":"2022-08-01T11:06:49.568Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"]}

View File

@ -0,0 +1,44 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"Hashtag": "as:Hashtag",
"quoteUrl": "as:quoteUrl",
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"featured": "toot:featured",
"discoverable": "toot:discoverable",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"misskey": "https://misskey-hub.net/ns#",
"_misskey_content": "misskey:_misskey_content",
"_misskey_quote": "misskey:_misskey_quote",
"_misskey_reaction": "misskey:_misskey_reaction",
"_misskey_votes": "misskey:_misskey_votes",
"_misskey_talk": "misskey:_misskey_talk",
"isCat": "misskey:isCat",
"vcard": "http://www.w3.org/2006/vcard/ns#"
}
],
"id": "https://mk.absturztau.be/notes/93e7nm8wqg",
"type": "Note",
"attributedTo": "https://mk.absturztau.be/users/8ozbzjs3o8",
"summary": null,
"content": "<p><span>meow</span></p>",
"_misskey_content": "meow",
"published": "2022-08-01T11:06:49.568Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://mk.absturztau.be/users/8ozbzjs3o8/followers"
],
"inReplyTo": null,
"attachment": [],
"sensitive": false,
"tag": []
}

View File

@ -0,0 +1,36 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://p.helene.moe/schemas/litepub-0.1.jsonld",
{
"@language": "und"
}
],
"actor": "https://p.helene.moe/users/helene",
"attachment": [],
"attributedTo": "https://p.helene.moe/users/helene",
"cc": [
"https://p.helene.moe/users/helene/followers"
],
"content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AHntpQ4T3J4OSnpgMC\" href=\"https://mk.absturztau.be/@mametsuko\" rel=\"ugc\">@<span>mametsuko</span></a></span> meow",
"context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d",
"id": "https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4",
"inReplyTo": "https://mk.absturztau.be/notes/93e7nm8wqg",
"published": "2022-08-02T13:46:58.403996Z",
"sensitive": null,
"source": "@mametsuko@mk.absturztau.be meow",
"summary": "",
"tag": [
{
"href": "https://mk.absturztau.be/users/8ozbzjs3o8",
"name": "@mametsuko@mk.absturztau.be",
"type": "Mention"
}
],
"to": [
"https://mk.absturztau.be/users/8ozbzjs3o8",
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note"
}

View File

@ -13,6 +13,29 @@ defmodule Pleroma.Activity.Ir.TopicsTest do
import Mock import Mock
describe "chat message" do
test "Create produces no topics" do
activity = %Activity{
object: %Object{data: %{"type" => "ChatMessage"}},
data: %{"type" => "Create"}
}
assert [] == Topics.get_activity_topics(activity)
end
test "Delete produces user and user:pleroma_chat" do
activity = %Activity{
object: %Object{data: %{"type" => "ChatMessage"}},
data: %{"type" => "Delete"}
}
topics = Topics.get_activity_topics(activity)
assert [_, _] = topics
assert "user" in topics
assert "user:pleroma_chat" in topics
end
end
describe "poll answer" do describe "poll answer" do
test "produce no topics" do test "produce no topics" do
activity = %Activity{object: %Object{data: %{"type" => "Answer"}}} activity = %Activity{object: %Object{data: %{"type" => "Answer"}}}
@ -35,7 +58,7 @@ test "always add user and list topics" do
setup do setup do
activity = %Activity{ activity = %Activity{
object: %Object{data: %{"type" => "Note"}}, object: %Object{data: %{"type" => "Note"}},
data: %{"to" => [Pleroma.Constants.as_public()]} data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"}
} }
{:ok, activity: activity} {:ok, activity: activity}
@ -114,6 +137,55 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d
end end
end end
describe "public visibility Announces" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => []}},
data: %{"type" => "Announce", "to" => [Pleroma.Constants.as_public()]}
}
{:ok, activity: activity}
end
test "does not generate public topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute "public" in topics
refute "public:remote" in topics
refute "public:local" in topics
end
end
describe "local-public visibility create events" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => []}},
data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
}
{:ok, activity: activity}
end
test "doesn't produce public topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public")
end
test "produces public:local topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:local")
end
test "with no attachments doesn't produce public:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public:media")
refute Enum.member?(topics, "public:local:media")
end
end
describe "public visibility create events with attachments" do describe "public visibility create events with attachments" do
setup do setup do
activity = %Activity{ activity = %Activity{
@ -152,9 +224,36 @@ test "non-local action produces public:remote:media topic", %{activity: activity
end end
end end
describe "local-public visibility create events with attachments" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => ["foo"]}},
data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
}
{:ok, activity: activity}
end
test "do not produce public:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public:media")
end
test "produces public:local:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:local:media")
end
end
describe "non-public visibility" do describe "non-public visibility" do
test "produces direct topic" do test "produces direct topic" do
activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} activity = %Activity{
object: %Object{data: %{"type" => "Note"}},
data: %{"to" => [], "type" => "Create"}
}
topics = Topics.get_activity_topics(activity) topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "direct") assert Enum.member?(topics, "direct")

View File

@ -278,4 +278,78 @@ test "add_by_params_query/3" do
assert Repo.aggregate(Activity, :count, :id) == 2 assert Repo.aggregate(Activity, :count, :id) == 2
end end
describe "associated_object_id() sql function" do
test "with json object" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{"object": {"id":"foobar"}}'::jsonb);
"""
)
assert object_id == "foobar"
end
test "with string object" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{"object": "foobar"}'::jsonb);
"""
)
assert object_id == "foobar"
end
test "with array object" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{"object": ["foobar", {}]}'::jsonb);
"""
)
assert object_id == "foobar"
end
test "invalid" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{"object": {}}'::jsonb);
"""
)
assert is_nil(object_id)
end
test "invalid object id" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{"object": {"id": 123}}'::jsonb);
"""
)
assert is_nil(object_id)
end
test "no object field" do
%{rows: [[object_id]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"""
select associated_object_id('{}'::jsonb);
"""
)
assert is_nil(object_id)
end
end
end end

View File

@ -79,35 +79,70 @@ test "transfer config values with full subkey update" do
describe "pleroma restart" do describe "pleroma restart" do
setup do setup do
on_exit(fn -> Restarter.Pleroma.refresh() end) on_exit(fn ->
Restarter.Pleroma.refresh()
# Restarter.Pleroma.refresh/0 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
# See https://stackoverflow.com/questions/51361856/how-to-use-task-await-with-genserver
Restarter.Pleroma.rebooted?()
end)
end end
@tag :erratic
test "don't restart if no reboot time settings were changed" do test "don't restart if no reboot time settings were changed" do
clear_config(:emoji) clear_config(:emoji)
insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
refute String.contains?( refute String.contains?(
capture_log(fn -> TransferTask.start_link([]) end), capture_log(fn ->
TransferTask.start_link([])
# TransferTask.start_link/1 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?()
end),
"pleroma restarted" "pleroma restarted"
) )
end end
@tag :erratic
test "on reboot time key" do test "on reboot time key" do
clear_config(:shout) clear_config(:shout)
insert(:config, key: :shout, value: [enabled: false]) insert(:config, key: :shout, value: [enabled: false])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
# Note that we don't actually restart Pleroma.
# See module Restarter.Pleroma
assert capture_log(fn ->
TransferTask.start_link([])
# TransferTask.start_link/1 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?()
end) =~ "pleroma restarted"
end end
@tag :erratic
test "on reboot time subkey" do test "on reboot time subkey" do
clear_config(Pleroma.Captcha) clear_config(Pleroma.Captcha)
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
# Note that we don't actually restart Pleroma.
# See module Restarter.Pleroma
assert capture_log(fn ->
TransferTask.start_link([])
# TransferTask.start_link/1 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?()
end) =~ "pleroma restarted"
end end
@tag :erratic
test "don't restart pleroma on reboot time key and subkey if there is false flag" do test "don't restart pleroma on reboot time key and subkey if there is false flag" do
clear_config(:shout) clear_config(:shout)
clear_config(Pleroma.Captcha) clear_config(Pleroma.Captcha)
@ -116,7 +151,15 @@ test "don't restart pleroma on reboot time key and subkey if there is false flag
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
refute String.contains?( refute String.contains?(
capture_log(fn -> TransferTask.load_and_update_env([], false) end), capture_log(fn ->
TransferTask.load_and_update_env([], false)
# TransferTask.start_link/1 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?()
end),
"pleroma restarted" "pleroma restarted"
) )
end end

View File

@ -122,11 +122,11 @@ test "recreating an existing participations sets it to unread" do
end end
test "it marks a participation as read" do test "it marks a participation as read" do
participation = insert(:participation, %{read: false}) participation = insert(:participation, %{updated_at: ~N[2017-07-17 17:09:58], read: false})
{:ok, updated_participation} = Participation.mark_as_read(participation) {:ok, updated_participation} = Participation.mark_as_read(participation)
assert updated_participation.read assert updated_participation.read
assert updated_participation.updated_at == participation.updated_at assert :gt = NaiveDateTime.compare(updated_participation.updated_at, participation.updated_at)
end end
test "it marks a participation as unread" do test "it marks a participation as unread" do

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