Merge branch 'develop' into 'from/upstream-develop/tusooa/2892-backup-scope'
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
9874b4c985
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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`:
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 woman’s clothes
|
1F45A ; fully-qualified # 👚 E0.6 woman’s 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 woman’s sandal
|
1F461 ; fully-qualified # 👡 E0.6 woman’s sandal
|
||||||
1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
|
1FA70 ; fully-qualified # 🩰 E12.0 ballet shoes
|
||||||
1F462 ; fully-qualified # 👢 E0.6 woman’s boot
|
1F462 ; fully-qualified # 👢 E0.6 woman’s 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 woman’s hat
|
1F452 ; fully-qualified # 👒 E0.6 woman’s 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
|
||||||
|
|
|
@ -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
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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),
|
||||||
|
|
|
@ -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" => [
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"] || ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
{:ok, new_object} =
|
||||||
|
Object.Updater.do_with_history(message["object"], fn object ->
|
||||||
|
hashtags = Object.hashtags(%Object{data: object})
|
||||||
|
|
||||||
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
|
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
|
||||||
{:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
|
{:ok, Map.put(object, "sensitive", true)}
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, object}
|
||||||
end
|
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
|
||||||
|
|
|
@ -27,6 +27,8 @@ defp object_payload(%{} = object) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_reject(%{"object" => %{} = object} = message) do
|
defp check_reject(%{"object" => %{} = object} = message) do
|
||||||
|
with {:ok, _new_object} <-
|
||||||
|
Pleroma.Object.Updater.do_with_history(object, fn object ->
|
||||||
payload = object_payload(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 ->
|
||||||
|
@ -36,15 +38,35 @@ defp check_reject(%{"object" => %{} = object} = message) do
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
|
end) do
|
||||||
|
{:ok, message}
|
||||||
|
else
|
||||||
|
e -> e
|
||||||
|
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
|
||||||
|
check_keyword = fn object ->
|
||||||
payload = object_payload(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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -127,6 +127,8 @@ def validate(%{"type" => type} = object, meta)
|
||||||
"Page" -> ArticleNotePageValidator
|
"Page" -> ArticleNotePageValidator
|
||||||
end
|
end
|
||||||
|
|
||||||
|
with {:ok, object} <-
|
||||||
|
do_separate_with_history(object, fn object ->
|
||||||
with {:ok, object} <-
|
with {:ok, object} <-
|
||||||
object
|
object
|
||||||
|> validator.cast_and_validate()
|
|> validator.cast_and_validate()
|
||||||
|
@ -137,10 +139,42 @@ def validate(%{"type" => type} = object, meta)
|
||||||
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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,24 +154,27 @@ 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
|
else
|
||||||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
|
handle_update_object(object, meta)
|
||||||
|
|
||||||
User.get_by_ap_id(updated_object["id"])
|
|
||||||
|> User.remote_user_changeset(new_user_data)
|
|
||||||
|> User.update_and_set_cache()
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
# - Add like to object
|
# - Add like to object
|
||||||
|
@ -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"])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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" => [],
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, %{})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
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)
|
<<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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -25,21 +25,58 @@ def call(conn, _opts) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_assign_valid_signature(conn) do
|
defp validate_signature(conn, request_target) do
|
||||||
if has_signature_header?(conn) do
|
# Newer drafts for HTTP signatures now use @request-target instead of the
|
||||||
# set (request-target) header to the appropriate value
|
# old (request-target). We'll now support both for incoming signatures.
|
||||||
# we also replace the digest header with the one we computed
|
|
||||||
request_target = String.downcase("#{conn.method}") <> " #{conn.request_path}"
|
|
||||||
|
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> put_req_header("(request-target)", request_target)
|
|> put_req_header("(request-target)", request_target)
|
||||||
|> case do
|
|> 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
|
||||||
|
if has_signature_header?(conn) do
|
||||||
|
# we replace the digest header with the one we computed in DigestPlug
|
||||||
|
conn =
|
||||||
|
case conn 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 %>
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -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]
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 ""
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -13,7 +13,8 @@ def project do
|
||||||
|
|
||||||
def application do
|
def application do
|
||||||
[
|
[
|
||||||
mod: {Restarter, []}
|
mod: {Restarter, []},
|
||||||
|
extra_applications: [:logger]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"]}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue