Merge remote-tracking branch 'origin/develop' into shigusegubu
* origin/develop: (208 commits) Document inclusion of wasm-unsafe-eval Update http_security_plug.ex Update lib/pleroma/web/plugs/http_security_plug.ex try to fix ruffle on chrome Alter priority of Delete activities to be lowest Deletes do not generate notifications of any kind, so skip trying Object.Fetcher: Set reachable on successful fetch Translated using Weblate (Ukrainian) Added translation using Weblate (Ukrainian) Added translation using Weblate (Ukrainian) Lint Update config cheatsheet Clarify config description Document Oban workers getting timeouts defined Lint Define sane Oban Worker timeouts Clean up stale entries in mix.lock Document removal of Quack Add migration to remove Quack from ConfigDB Remove Quack logging backend ...
This commit is contained in:
commit
447bfcdb35
251 changed files with 14728 additions and 1185 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -48,6 +48,7 @@ docs/generated_config.md
|
||||||
# Code test coverage
|
# Code test coverage
|
||||||
/cover
|
/cover
|
||||||
/Elixir.*.coverdata
|
/Elixir.*.coverdata
|
||||||
|
/coverage.xml
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
pleroma.iml
|
pleroma.iml
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ variables: &global_variables
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
DB_HOST: postgres
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
|
||||||
cache: &global_cache_policy
|
cache: &global_cache_policy
|
||||||
|
|
@ -84,7 +85,13 @@ unit-testing:
|
||||||
script:
|
script:
|
||||||
- mix ecto.create
|
- mix ecto.create
|
||||||
- mix ecto.migrate
|
- mix ecto.migrate
|
||||||
- mix coveralls --preload-modules
|
- mix test --cover --preload-modules
|
||||||
|
coverage: '/^Line total: ([^ ]*%)$/'
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
unit-testing-erratic:
|
unit-testing-erratic:
|
||||||
stage: test
|
stage: test
|
||||||
|
|
@ -271,7 +278,7 @@ amd64:
|
||||||
MIX_ENV: prod
|
MIX_ENV: prod
|
||||||
before_script: &before-release
|
before_script: &before-release
|
||||||
- apt-get update && apt-get install -y cmake libmagic-dev
|
- apt-get update && apt-get install -y cmake libmagic-dev
|
||||||
- echo "import Mix.Config" > config/prod.secret.exs
|
- echo "import Config" > config/prod.secret.exs
|
||||||
- mix local.hex --force
|
- mix local.hex --force
|
||||||
- mix local.rebar --force
|
- mix local.rebar --force
|
||||||
script: &release
|
script: &release
|
||||||
|
|
@ -290,7 +297,7 @@ amd64-musl:
|
||||||
variables: *release-variables
|
variables: *release-variables
|
||||||
before_script: &before-release-musl
|
before_script: &before-release-musl
|
||||||
- apk add git build-base cmake file-dev openssl
|
- apk add git build-base cmake file-dev openssl
|
||||||
- echo "import Mix.Config" > config/prod.secret.exs
|
- echo "import Config" > config/prod.secret.exs
|
||||||
- mix local.hex --force
|
- mix local.hex --force
|
||||||
- mix local.rebar --force
|
- mix local.rebar --force
|
||||||
script: *release
|
script: *release
|
||||||
|
|
|
||||||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -11,8 +11,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- MastoFE
|
- MastoFE
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Breaking:** Elixir >=1.10 is now required (was >= 1.9)
|
||||||
- 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`
|
||||||
|
- Updated the recommended pleroma.vcl configuration for Varnish to target Varnish 7.0+
|
||||||
|
- Set timeout values for Oban queues. The default is infinity and some operations may not time out on their own.
|
||||||
|
- Delete activities are federated at lowest priority
|
||||||
|
- CSP now includes wasm-unsafe-eval
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object
|
- `activeMonth` and `activeHalfyear` fields in NodeInfo usage.users object
|
||||||
|
|
@ -33,6 +39,8 @@ 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
|
||||||
|
- Possibility to discover users like `user@example.org`, while Pleroma is working on `pleroma.example.org`. Additional configuration required.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
|
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
|
||||||
|
|
@ -48,8 +56,16 @@ 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 useless notification call on Delete activities
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
- Quack, the logging backend that pushes to Slack channels
|
||||||
|
|
||||||
|
## 2.4.4 - 2022-08-19
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Streaming API sessions will now properly disconnect if the corresponding token is revoked
|
||||||
|
|
||||||
## 2.4.3 - 2022-05-06
|
## 2.4.3 - 2022-05-06
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
FROM elixir:1.9-alpine as build
|
FROM elixir:1.10-alpine as build
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV MIX_ENV=prod
|
ENV MIX_ENV=prod
|
||||||
|
|
||||||
RUN apk add git gcc g++ musl-dev make cmake file-dev &&\
|
RUN apk add git gcc g++ musl-dev make cmake file-dev &&\
|
||||||
echo "import Mix.Config" > config/prod.secret.exs &&\
|
echo "import Config" > config/prod.secret.exs &&\
|
||||||
mix local.hex --force &&\
|
mix local.hex --force &&\
|
||||||
mix local.rebar --force &&\
|
mix local.rebar --force &&\
|
||||||
mix deps.get --only prod &&\
|
mix deps.get --only prod &&\
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
FROM elixir:1.9.4
|
FROM elixir:1.10.4
|
||||||
|
|
||||||
|
# Single RUN statement, otherwise intermediate images are created
|
||||||
|
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
|
||||||
RUN apt-get update &&\
|
RUN apt-get update &&\
|
||||||
apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
|
apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
|
||||||
mix local.hex --force &&\
|
mix local.hex --force &&\
|
||||||
mix local.rebar --force
|
mix local.rebar --force
|
||||||
|
|
||||||
|
|
|
||||||
12
ci/README
Normal file
12
ci/README
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Assuming an AMD64 Alpine system, you're going to need the following packages
|
||||||
|
- `qemu qemu-openrc qemu-arm qemu-aarch64` for binfmt
|
||||||
|
- `docker-cli-buildx` for building the images
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
```
|
||||||
|
docker login git.pleroma.social:5050
|
||||||
|
doas rc-service qemu-binfmt start
|
||||||
|
```
|
||||||
|
|
@ -40,6 +40,7 @@ config :pleroma, Pleroma.Repo,
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
database: "pleroma_benchmark",
|
database: "pleroma_benchmark",
|
||||||
hostname: System.get_env("DB_HOST") || "localhost",
|
hostname: System.get_env("DB_HOST") || "localhost",
|
||||||
|
port: System.get_env("DB_PORT") || "5432",
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
# Reduce hash rounds for testing
|
# Reduce hash rounds for testing
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
|
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
|
||||||
#
|
#
|
||||||
# This file is responsible for configuring your application
|
# This file is responsible for configuring your application
|
||||||
# and its dependencies with the aid of the Mix.Config module.
|
# and its dependencies with the aid of the Config module.
|
||||||
#
|
#
|
||||||
# This configuration file is loaded before any dependency and
|
# This configuration file is loaded before any dependency and
|
||||||
# is restricted to this project.
|
# is restricted to this project.
|
||||||
|
|
@ -160,11 +160,6 @@ config :logger, :ex_syslogger,
|
||||||
format: "$metadata[$level] $message",
|
format: "$metadata[$level] $message",
|
||||||
metadata: [:request_id]
|
metadata: [:request_id]
|
||||||
|
|
||||||
config :quack,
|
|
||||||
level: :warn,
|
|
||||||
meta: [:all],
|
|
||||||
webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
|
|
||||||
|
|
||||||
config :mime, :types, %{
|
config :mime, :types, %{
|
||||||
"application/xml" => ["xml"],
|
"application/xml" => ["xml"],
|
||||||
"application/xrd+xml" => ["xrd+xml"],
|
"application/xrd+xml" => ["xrd+xml"],
|
||||||
|
|
@ -226,6 +221,7 @@ config :pleroma, :instance,
|
||||||
max_pinned_statuses: 1,
|
max_pinned_statuses: 1,
|
||||||
attachment_links: true,
|
attachment_links: true,
|
||||||
max_report_comment_size: 1000,
|
max_report_comment_size: 1000,
|
||||||
|
report_strip_status: true,
|
||||||
safe_dm_mentions: false,
|
safe_dm_mentions: false,
|
||||||
healthcheck: false,
|
healthcheck: false,
|
||||||
remote_post_retention_days: 90,
|
remote_post_retention_days: 90,
|
||||||
|
|
@ -572,8 +568,8 @@ config :pleroma, Oban,
|
||||||
token_expiration: 5,
|
token_expiration: 5,
|
||||||
filter_expiration: 1,
|
filter_expiration: 1,
|
||||||
backup: 1,
|
backup: 1,
|
||||||
federator_incoming: 50,
|
federator_incoming: 5,
|
||||||
federator_outgoing: 50,
|
federator_outgoing: 5,
|
||||||
ingestion_queue: 50,
|
ingestion_queue: 50,
|
||||||
web_push: 50,
|
web_push: 50,
|
||||||
mailer: 10,
|
mailer: 10,
|
||||||
|
|
@ -686,6 +682,8 @@ config :pleroma, :features, improved_hashtag_timeline: :auto
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -754,7 +752,7 @@ config :pleroma, :frontends,
|
||||||
"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", "/"}
|
||||||
|
|
@ -767,13 +765,21 @@ config :pleroma, :frontends,
|
||||||
"https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build",
|
"https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build",
|
||||||
"ref" => "develop"
|
"ref" => "develop"
|
||||||
},
|
},
|
||||||
"soapbox-fe" => %{
|
"soapbox" => %{
|
||||||
"name" => "soapbox-fe",
|
"name" => "soapbox",
|
||||||
"git" => "https://gitlab.com/soapbox-pub/soapbox-fe",
|
"git" => "https://gitlab.com/soapbox-pub/soapbox",
|
||||||
"build_url" =>
|
"build_url" =>
|
||||||
"https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production",
|
"https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/${ref}/download?job=build-production",
|
||||||
"ref" => "v1.0.0",
|
"ref" => "v3.0.0-beta.1",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -877,6 +883,8 @@ config :pleroma, ConcurrentLimiter, [
|
||||||
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
|
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{Mix.env()}.exs"
|
import_config "#{Mix.env()}.exs"
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,27 @@ config :pleroma, :config_description, [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
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,
|
||||||
|
|
@ -794,6 +815,13 @@ config :pleroma, :config_description, [
|
||||||
1_000
|
1_000
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :report_strip_status,
|
||||||
|
label: "Report strip status",
|
||||||
|
type: :boolean,
|
||||||
|
description:
|
||||||
|
"Strip associated statuses in reports to ids when closed/resolved, otherwise keep a copy"
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :safe_dm_mentions,
|
key: :safe_dm_mentions,
|
||||||
label: "Safe DM mentions",
|
label: "Safe DM mentions",
|
||||||
|
|
@ -984,7 +1012,8 @@ config :pleroma, :config_description, [
|
||||||
key: :birthday_min_age,
|
key: :birthday_min_age,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
description:
|
description:
|
||||||
"Minimum required age for users to create account. Only used if birthday is required."
|
"Minimum required age (in days) for users to create account. Only used if birthday is required.",
|
||||||
|
suggestions: [6570]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1168,45 +1197,6 @@ config :pleroma, :config_description, [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
group: :quack,
|
|
||||||
type: :group,
|
|
||||||
label: "Quack Logger",
|
|
||||||
description: "Quack-related settings",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :level,
|
|
||||||
type: {:dropdown, :atom},
|
|
||||||
description: "Log level",
|
|
||||||
suggestions: [:debug, :info, :warn, :error]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :meta,
|
|
||||||
type: {:list, :atom},
|
|
||||||
description: "Configure which metadata you want to report on",
|
|
||||||
suggestions: [
|
|
||||||
:application,
|
|
||||||
:module,
|
|
||||||
:file,
|
|
||||||
:function,
|
|
||||||
:line,
|
|
||||||
:pid,
|
|
||||||
:crash_reason,
|
|
||||||
:initial_call,
|
|
||||||
:registered_name,
|
|
||||||
:all,
|
|
||||||
:none
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :webhook_url,
|
|
||||||
label: "Webhook URL",
|
|
||||||
type: :string,
|
|
||||||
description: "Configure the Slack incoming webhook",
|
|
||||||
suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :frontend_configurations,
|
key: :frontend_configurations,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ config :pleroma, Pleroma.Repo,
|
||||||
password: System.fetch_env!("DB_PASS"),
|
password: System.fetch_env!("DB_PASS"),
|
||||||
database: System.get_env("DB_NAME", "pleroma"),
|
database: System.get_env("DB_NAME", "pleroma"),
|
||||||
hostname: System.get_env("DB_HOST", "db"),
|
hostname: System.get_env("DB_HOST", "db"),
|
||||||
|
port: System.get_env("DB_PORT", "5432"),
|
||||||
pool_size: 10
|
pool_size: 10
|
||||||
|
|
||||||
# Configure web push notifications
|
# Configure web push notifications
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ config :pleroma, Pleroma.Repo,
|
||||||
password: "postgres",
|
password: "postgres",
|
||||||
database: "pleroma_test",
|
database: "pleroma_test",
|
||||||
hostname: System.get_env("DB_HOST") || "localhost",
|
hostname: System.get_env("DB_HOST") || "localhost",
|
||||||
|
port: System.get_env("DB_PORT") || "5432",
|
||||||
pool: Ecto.Adapters.SQL.Sandbox,
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
pool_size: 50
|
pool_size: 50
|
||||||
|
|
||||||
|
|
@ -129,6 +130,8 @@ config :pleroma, :pipeline,
|
||||||
|
|
||||||
config :pleroma, :cachex, provider: Pleroma.CachexMock
|
config :pleroma, :cachex, provider: Pleroma.CachexMock
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.WebFinger, update_nickname_on_user_fetch: false
|
||||||
|
|
||||||
config :pleroma, :side_effects,
|
config :pleroma, :side_effects,
|
||||||
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
|
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
|
||||||
logger: Pleroma.LoggerMock
|
logger: Pleroma.LoggerMock
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "-- Waiting for database..."
|
echo "-- Waiting for database..."
|
||||||
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:5432/${DB_NAME:-pleroma} -t 1; do
|
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
|
||||||
sleep 1s
|
sleep 1s
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Currently, known `<frontend>` values are:
|
||||||
- [kenoma](http://git.pleroma.social/lambadalambda/kenoma)
|
- [kenoma](http://git.pleroma.social/lambadalambda/kenoma)
|
||||||
- [pleroma-fe](http://git.pleroma.social/pleroma/pleroma-fe)
|
- [pleroma-fe](http://git.pleroma.social/pleroma/pleroma-fe)
|
||||||
- [fedi-fe](https://git.pleroma.social/pleroma/fedi-fe)
|
- [fedi-fe](https://git.pleroma.social/pleroma/fedi-fe)
|
||||||
- [soapbox-fe](https://gitlab.com/soapbox-pub/soapbox-fe)
|
- [soapbox](https://gitlab.com/soapbox-pub/soapbox)
|
||||||
|
|
||||||
You can still install frontends that are not configured, see below.
|
You can still install frontends that are not configured, see below.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `autofollowing_nicknames`: Set to nicknames of (local) users that automatically follows every newly registered user.
|
* `autofollowing_nicknames`: Set to nicknames of (local) users that automatically follows every newly registered user.
|
||||||
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
|
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
|
||||||
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
|
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
|
||||||
|
* `report_strip_status`: Strip associated statuses in reports to ids when closed/resolved, otherwise keep a copy.
|
||||||
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
|
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
|
||||||
* `healthcheck`: If set to true, system data will be shown on ``/api/v1/pleroma/healthcheck``.
|
* `healthcheck`: If set to true, system data will be shown on ``/api/v1/pleroma/healthcheck``.
|
||||||
* `remote_post_retention_days`: The default amount of days to retain remote posts when pruning the database.
|
* `remote_post_retention_days`: The default amount of days to retain remote posts when pruning the database.
|
||||||
|
|
@ -204,7 +205,7 @@ config :pleroma, :mrf_user_allowlist, %{
|
||||||
e.g., A value of 900 results in any post with a timestamp older than 15 minutes will be acted upon.
|
e.g., A value of 900 results in any post with a timestamp older than 15 minutes will be acted upon.
|
||||||
* `actions`: A list of actions to apply to the post:
|
* `actions`: A list of actions to apply to the post:
|
||||||
* `:delist` removes the post from public timelines
|
* `:delist` removes the post from public timelines
|
||||||
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
|
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message
|
||||||
* `:reject` rejects the message entirely
|
* `:reject` rejects the message entirely
|
||||||
|
|
||||||
#### :mrf_steal_emoji
|
#### :mrf_steal_emoji
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# How to use a different domain name for Pleroma and the users it serves
|
||||||
|
|
||||||
|
Pleroma users are primarily identified by a `user@example.org` handle, and you might want this identifier to be the same as your email or jabber account, for instance.
|
||||||
|
However, in this case, you are almost certainly serving some web content on `https://example.org` already, and you might want to use another domain (say `pleroma.example.org`) for Pleroma itself.
|
||||||
|
|
||||||
|
Pleroma supports that, but it might be tricky to set up, and any error might prevent you from federating with other instances.
|
||||||
|
|
||||||
|
*If you are already running Pleroma on `example.org`, it is no longer possible to move it to `pleroma.example.org`.*
|
||||||
|
|
||||||
|
## Account identifiers
|
||||||
|
|
||||||
|
It is important to understand that for federation purposes, a user in Pleroma has two unique identifiers associated:
|
||||||
|
|
||||||
|
- A webfinger `acct:` URI, used for discovery and as a verifiable global name for the user across Pleroma instances. In our example, our account's acct: URI is `acct:user@example.org`
|
||||||
|
- An author/actor URI, used in every other aspect of federation. This is the way in which users are identified in ActivityPub, the underlying protocol used for federation with other Pleroma instances.
|
||||||
|
In our case, it is `https://pleroma.example.org/users/user`.
|
||||||
|
|
||||||
|
Both account identifiers are unique and required for Pleroma. An important risk if you set up your Pleroma instance incorrectly is to create two users (with different acct: URIs) with conflicting author/actor URIs.
|
||||||
|
|
||||||
|
## WebFinger
|
||||||
|
|
||||||
|
As said earlier, each Pleroma user has an `acct`: URI, which is used for discovery and authentication. When you add @user@example.org, a webfinger query is performed. This is done in two steps:
|
||||||
|
|
||||||
|
1. Querying `https://example.org/.well-known/host-meta` (where the domain of the URL matches the domain part of the `acct`: URI) to get information on how to perform the query.
|
||||||
|
This file will indeed contain a URL template of the form `https://example.org/.well-known/webfinger?resource={uri}` that will be used in the second step.
|
||||||
|
2. Fill the returned template with the `acct`: URI to be queried and perform the query: `https://example.org/.well-known/webfinger?resource=acct:user@example.org`
|
||||||
|
|
||||||
|
## Configuring your Pleroma instance
|
||||||
|
|
||||||
|
**_DO NOT ATTEMPT TO CONFIGURE YOUR INSTANCE THIS WAY IF YOU DID NOT UNDERSTAND THE ABOVE_**
|
||||||
|
|
||||||
|
### Configuring Pleroma
|
||||||
|
|
||||||
|
Pleroma has a two configuration settings to enable using different domains for your users and Pleroma itself. `host` in `Pleroma.Web.Endpoint` and `domain` in `Pleroma.Web.WebFinger`. When the latter is not set, it defaults to the value of `host`.
|
||||||
|
|
||||||
|
*Be extra careful when configuring your Pleroma instance, as changing `host` may cause remote instances to register different accounts with the same author/actor URI, which will result in federation issues!*
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :pleroma, Pleroma.Web.Endpoint,
|
||||||
|
url: [host: "pleroma.example.org"]
|
||||||
|
|
||||||
|
config :pleroma, Pleroma.Web.WebFinger, domain: "example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `domain` - is the domain for which your Pleroma instance has authority, it's the domain used in `acct:` URI. In our example, `domain` would be set to `example.org`. This is used in WebFinger account ids, which are the canonical account identifier in some other fediverse software like Mastodon. **If you change `domain`, the accounts on your server will be shown as different accounts in those software**.
|
||||||
|
- `host` - is the domain used for any URL generated for your instance, including the author/actor URL's. In our case, that would be `pleroma.example.org`. This is used in AP ids, which are the canonical account identifier in Pleroma and some other fediverse software. **You should not change this after you have set up the instance**.
|
||||||
|
|
||||||
|
### Configuring WebFinger domain
|
||||||
|
|
||||||
|
Now, you have Pleroma running at `https://pleroma.example.org` as well as a website at `https://example.org`. If you recall how webfinger queries work, the first step is to query `https://example.org/.well-known/host-meta`, which will contain an URL template.
|
||||||
|
|
||||||
|
Therefore, the easiest way to configure `example.org` is to redirect `/.well-known/host-meta` to `pleroma.example.org`.
|
||||||
|
|
||||||
|
With nginx, it would be as simple as adding:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location = /.well-known/host-meta {
|
||||||
|
return 301 https://pleroma.example.org$request_uri;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
in example.org's server block.
|
||||||
|
|
@ -59,7 +59,7 @@ The configuration of Pleroma has traditionally been managed with a config file,
|
||||||
Here is an example of a server config stripped down after migration:
|
Here is an example of a server config stripped down after migration:
|
||||||
|
|
||||||
```
|
```
|
||||||
use Mix.Config
|
import Config
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.Endpoint,
|
config :pleroma, Pleroma.Web.Endpoint,
|
||||||
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
|
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
|
||||||
|
|
|
||||||
|
|
@ -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,7 +1,7 @@
|
||||||
## Required dependencies
|
## Required dependencies
|
||||||
|
|
||||||
* PostgreSQL 9.6+
|
* PostgreSQL 9.6+
|
||||||
* Elixir 1.9+
|
* Elixir 1.10+
|
||||||
* Erlang OTP 22.2+
|
* Erlang OTP 22.2+
|
||||||
* git
|
* git
|
||||||
* file / libmagic
|
* file / libmagic
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
elixir_version=1.9.4
|
elixir_version=1.10.4
|
||||||
erlang_version=22.3.4.1
|
erlang_version=22.3.4.1
|
||||||
|
|
|
||||||
|
|
@ -466,7 +466,7 @@
|
||||||
|
|
||||||
%% == PostgreSQL ==
|
%% == PostgreSQL ==
|
||||||
%% {rdbms, global, default, [{workers, 10}],
|
%% {rdbms, global, default, [{workers, 10}],
|
||||||
%% [{server, {pgsql, "server", 5432, "database", "username", "password"}}]},
|
%% [{server, {pgsql, "server", "port", "database", "username", "password"}}]},
|
||||||
|
|
||||||
%% == ODBC (MSSQL) ==
|
%% == ODBC (MSSQL) ==
|
||||||
%% {rdbms, global, default, [{workers, 10}],
|
%% {rdbms, global, default, [{workers, 10}],
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -304,13 +304,8 @@ defmodule Mix.Tasks.Pleroma.Config do
|
||||||
System.cmd("mix", ["format", path])
|
System.cmd("mix", ["format", path])
|
||||||
end
|
end
|
||||||
|
|
||||||
if Code.ensure_loaded?(Config.Reader) do
|
defp config_header, do: "import Config\r\n\r\n"
|
||||||
defp config_header, do: "import Config\r\n\r\n"
|
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
|
||||||
defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
|
|
||||||
else
|
|
||||||
defp config_header, do: "use Mix.Config\r\n\r\n"
|
|
||||||
defp read_file(config_file), do: Mix.Config.eval!(config_file)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp write_and_delete(config, file, delete?) do
|
defp write_and_delete(config, file, delete?) do
|
||||||
config
|
config
|
||||||
|
|
|
||||||
|
|
@ -154,9 +154,8 @@ defmodule Mix.Tasks.Pleroma.Database 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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ defmodule Mix.Tasks.Pleroma.User do
|
||||||
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||||
shell_info("Generated password reset token for #{user.nickname}")
|
shell_info("Generated password reset token for #{user.nickname}")
|
||||||
|
|
||||||
IO.puts("URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint,
|
url =
|
||||||
:reset,
|
Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint, :reset, token.token)
|
||||||
token.token)}")
|
|
||||||
|
IO.puts("URL: #{url}")
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
shell_error("No local user #{nickname}")
|
shell_error("No local user #{nickname}")
|
||||||
|
|
@ -421,6 +422,38 @@ defmodule Mix.Tasks.Pleroma.User 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 @@ defmodule Pleroma.Activity 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 @@ defmodule Pleroma.Activity.HTML do
|
||||||
|
|
||||||
@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 @@ defmodule Pleroma.Activity.Ir.Topics 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 @@ defmodule Pleroma.Activity.Ir.Topics 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 @@ defmodule Pleroma.Activity.Ir.Topics 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 @@ defmodule Pleroma.Activity.Ir.Topics 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 @@ defmodule Pleroma.Activity.Ir.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 @@ defmodule Pleroma.Activity.Queries 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 @@ defmodule Pleroma.Activity.Queries 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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ defmodule Pleroma.Activity.Search do
|
||||||
Activity
|
Activity
|
||||||
|> Activity.with_preloaded_object()
|
|> Activity.with_preloaded_object()
|
||||||
|> Activity.restrict_deactivated_users()
|
|> Activity.restrict_deactivated_users()
|
||||||
|> restrict_public()
|
|> restrict_public(user)
|
||||||
|> query_with(index_type, search_query, search_function)
|
|> query_with(index_type, search_query, search_function)
|
||||||
|> maybe_restrict_local(user)
|
|> maybe_restrict_local(user)
|
||||||
|> maybe_restrict_author(author)
|
|> maybe_restrict_author(author)
|
||||||
|
|
@ -57,7 +57,19 @@ defmodule Pleroma.Activity.Search do
|
||||||
|
|
||||||
def maybe_restrict_blocked(query, _), do: query
|
def maybe_restrict_blocked(query, _), do: query
|
||||||
|
|
||||||
defp restrict_public(q) do
|
defp restrict_public(q, user) when not is_nil(user) do
|
||||||
|
intended_recipients = [
|
||||||
|
Pleroma.Constants.as_public(),
|
||||||
|
Pleroma.Web.ActivityPub.Utils.as_local_public()
|
||||||
|
]
|
||||||
|
|
||||||
|
from([a, o] in q,
|
||||||
|
where: fragment("?->>'type' = 'Create'", a.data),
|
||||||
|
where: fragment("? && ?", ^intended_recipients, a.recipients)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_public(q, _user) do
|
||||||
from([a, o] in q,
|
from([a, o] in q,
|
||||||
where: fragment("?->>'type' = 'Create'", a.data),
|
where: fragment("?->>'type' = 'Create'", a.data),
|
||||||
where: ^Pleroma.Constants.as_public() in a.recipients
|
where: ^Pleroma.Constants.as_public() in a.recipients
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ defmodule Pleroma.Application do
|
||||||
Pleroma.Repo,
|
Pleroma.Repo,
|
||||||
Config.TransferTask,
|
Config.TransferTask,
|
||||||
Pleroma.Emoji,
|
Pleroma.Emoji,
|
||||||
Pleroma.Web.Plugs.RateLimiter.Supervisor
|
Pleroma.Web.Plugs.RateLimiter.Supervisor,
|
||||||
|
{Task.Supervisor, name: Pleroma.TaskSupervisor}
|
||||||
] ++
|
] ++
|
||||||
cachex_children() ++
|
cachex_children() ++
|
||||||
http_children(adapter, @mix_env) ++
|
http_children(adapter, @mix_env) ++
|
||||||
|
|
@ -112,7 +113,17 @@ defmodule Pleroma.Application 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 +200,7 @@ defmodule Pleroma.Application 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 +250,8 @@ defmodule Pleroma.Application do
|
||||||
|
|
||||||
defp background_migrators do
|
defp background_migrators do
|
||||||
[
|
[
|
||||||
Pleroma.Migrators.HashtagsTableMigrator
|
Pleroma.Migrators.HashtagsTableMigrator,
|
||||||
|
Pleroma.Migrators.ContextObjectsDeletionMigrator
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,45 @@ defmodule Pleroma.BBS.Handler 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 @@ defmodule Pleroma.BBS.Handler 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 @@ defmodule Pleroma.BBS.Handler 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 @@ defmodule Pleroma.BBS.Handler 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 @@ defmodule Pleroma.BBS.Handler 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})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
||||||
|
|
||||||
warning_preface = """
|
warning_preface = """
|
||||||
!!!DEPRECATION WARNING!!!
|
!!!DEPRECATION WARNING!!!
|
||||||
Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
Your config is using old setting name `timeout` instead of `recv_timeout` in pool settings. The setting will not take effect until updated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
updated_config =
|
updated_config =
|
||||||
|
|
|
||||||
|
|
@ -19,21 +19,10 @@ defmodule Pleroma.Config.Loader do
|
||||||
:tesla
|
:tesla
|
||||||
]
|
]
|
||||||
|
|
||||||
if Code.ensure_loaded?(Config.Reader) do
|
@reader Config.Reader
|
||||||
@reader Config.Reader
|
|
||||||
|
|
||||||
def read(path), do: @reader.read!(path)
|
|
||||||
else
|
|
||||||
# support for Elixir less than 1.9
|
|
||||||
@reader Mix.Config
|
|
||||||
def read(path) do
|
|
||||||
path
|
|
||||||
|> @reader.eval!()
|
|
||||||
|> elem(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec read(Path.t()) :: keyword()
|
@spec read(Path.t()) :: keyword()
|
||||||
|
def read(path), do: @reader.read!(path)
|
||||||
|
|
||||||
@spec merge(keyword(), keyword()) :: keyword()
|
@spec merge(keyword(), keyword()) :: keyword()
|
||||||
def merge(c1, c2), do: @reader.merge(c1, c2)
|
def merge(c1, c2), do: @reader.merge(c1, c2)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ defmodule Pleroma.Config.TransferTask do
|
||||||
{logger, other} =
|
{logger, other} =
|
||||||
(Repo.all(ConfigDB) ++ deleted_settings)
|
(Repo.all(ConfigDB) ++ deleted_settings)
|
||||||
|> Enum.map(&merge_with_default/1)
|
|> Enum.map(&merge_with_default/1)
|
||||||
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
|
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger] end)
|
||||||
|
|
||||||
logger
|
logger
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|
|
@ -104,11 +104,6 @@ defmodule Pleroma.Config.TransferTask do
|
||||||
end
|
end
|
||||||
|
|
||||||
# change logger configuration in runtime, without restart
|
# change logger configuration in runtime, without restart
|
||||||
defp configure({:quack, key, _, merged}) do
|
|
||||||
Logger.configure_backend(Quack.Logger, [{key, merged}])
|
|
||||||
:ok = update_env(:quack, key, merged)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp configure({_, :backends, _, merged}) do
|
defp configure({_, :backends, _, merged}) do
|
||||||
# removing current backends
|
# removing current backends
|
||||||
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
|
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,6 @@ defmodule Pleroma.ConfigDB do
|
||||||
defp only_full_update?(%ConfigDB{group: group, key: key}) do
|
defp only_full_update?(%ConfigDB{group: group, key: key}) do
|
||||||
full_key_update = [
|
full_key_update = [
|
||||||
{:pleroma, :ecto_repos},
|
{:pleroma, :ecto_repos},
|
||||||
{:quack, :meta},
|
|
||||||
{:mime, :types},
|
{:mime, :types},
|
||||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||||
{:swarm, :node_blacklist},
|
{:swarm, :node_blacklist},
|
||||||
|
|
@ -386,7 +385,7 @@ defmodule Pleroma.ConfigDB do
|
||||||
|
|
||||||
@spec module_name?(String.t()) :: boolean()
|
@spec module_name?(String.t()) :: boolean()
|
||||||
def module_name?(string) do
|
def module_name?(string) do
|
||||||
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
|
Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Ueberauth|Swoosh)\./, string) or
|
||||||
string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
|
string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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 @@ defmodule Pleroma.DataMigration 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
|
||||||
|
|
|
||||||
|
|
@ -106,5 +106,12 @@ defmodule Pleroma.HTTP do
|
||||||
[Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
|
[Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp adapter_middlewares(_), do: []
|
defp adapter_middlewares(_) do
|
||||||
|
if Pleroma.Config.get(:env) == :test do
|
||||||
|
# Emulate redirects in test env, which are handled by adapters in other environments
|
||||||
|
[Tesla.Middleware.FollowRedirects]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
139
lib/pleroma/migrators/context_objects_deletion_migrator.ex
Normal file
139
lib/pleroma/migrators/context_objects_deletion_migrator.ex
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Migrators.ContextObjectsDeletionMigrator do
|
||||||
|
defmodule State do
|
||||||
|
use Pleroma.Migrators.Support.BaseMigratorState
|
||||||
|
|
||||||
|
@impl Pleroma.Migrators.Support.BaseMigratorState
|
||||||
|
defdelegate data_migration(), to: Pleroma.DataMigration, as: :delete_context_objects
|
||||||
|
end
|
||||||
|
|
||||||
|
use Pleroma.Migrators.Support.BaseMigrator
|
||||||
|
|
||||||
|
alias Pleroma.Migrators.Support.BaseMigrator
|
||||||
|
alias Pleroma.Object
|
||||||
|
|
||||||
|
@doc "This migration removes objects created exclusively for contexts, containing only an `id` field."
|
||||||
|
|
||||||
|
@impl BaseMigrator
|
||||||
|
def feature_config_path, do: [:features, :delete_context_objects]
|
||||||
|
|
||||||
|
@impl BaseMigrator
|
||||||
|
def fault_rate_allowance, do: Config.get([:delete_context_objects, :fault_rate_allowance], 0)
|
||||||
|
|
||||||
|
@impl BaseMigrator
|
||||||
|
def perform do
|
||||||
|
data_migration_id = data_migration_id()
|
||||||
|
max_processed_id = get_stat(:max_processed_id, 0)
|
||||||
|
|
||||||
|
Logger.info("Deleting context objects from `objects` (from oid: #{max_processed_id})...")
|
||||||
|
|
||||||
|
query()
|
||||||
|
|> where([object], object.id > ^max_processed_id)
|
||||||
|
|> Repo.chunk_stream(100, :batches, timeout: :infinity)
|
||||||
|
|> Stream.each(fn objects ->
|
||||||
|
object_ids = Enum.map(objects, & &1.id)
|
||||||
|
|
||||||
|
results = Enum.map(object_ids, &delete_context_object(&1))
|
||||||
|
|
||||||
|
failed_ids =
|
||||||
|
results
|
||||||
|
|> Enum.filter(&(elem(&1, 0) == :error))
|
||||||
|
|> Enum.map(&elem(&1, 1))
|
||||||
|
|
||||||
|
chunk_affected_count =
|
||||||
|
results
|
||||||
|
|> Enum.filter(&(elem(&1, 0) == :ok))
|
||||||
|
|> length()
|
||||||
|
|
||||||
|
for failed_id <- failed_ids do
|
||||||
|
_ =
|
||||||
|
Repo.query(
|
||||||
|
"INSERT INTO data_migration_failed_ids(data_migration_id, record_id) " <>
|
||||||
|
"VALUES ($1, $2) ON CONFLICT DO NOTHING;",
|
||||||
|
[data_migration_id, failed_id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
_ =
|
||||||
|
Repo.query(
|
||||||
|
"DELETE FROM data_migration_failed_ids " <>
|
||||||
|
"WHERE data_migration_id = $1 AND record_id = ANY($2)",
|
||||||
|
[data_migration_id, object_ids -- failed_ids]
|
||||||
|
)
|
||||||
|
|
||||||
|
max_object_id = Enum.at(object_ids, -1)
|
||||||
|
|
||||||
|
put_stat(:max_processed_id, max_object_id)
|
||||||
|
increment_stat(:iteration_processed_count, length(object_ids))
|
||||||
|
increment_stat(:processed_count, length(object_ids))
|
||||||
|
increment_stat(:failed_count, length(failed_ids))
|
||||||
|
increment_stat(:affected_count, chunk_affected_count)
|
||||||
|
put_stat(:records_per_second, records_per_second())
|
||||||
|
persist_state()
|
||||||
|
|
||||||
|
# A quick and dirty approach to controlling the load this background migration imposes
|
||||||
|
sleep_interval = Config.get([:delete_context_objects, :sleep_interval_ms], 0)
|
||||||
|
Process.sleep(sleep_interval)
|
||||||
|
end)
|
||||||
|
|> Stream.run()
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl BaseMigrator
|
||||||
|
def query do
|
||||||
|
# Context objects have no activity type, and only one field, `id`.
|
||||||
|
# Only those context objects are without types.
|
||||||
|
from(
|
||||||
|
object in Object,
|
||||||
|
where: fragment("(?)->'type' IS NULL", object.data),
|
||||||
|
select: %{
|
||||||
|
id: object.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete_context_object(integer()) :: {:ok | :error, integer()}
|
||||||
|
defp delete_context_object(id) do
|
||||||
|
result =
|
||||||
|
%Object{id: id}
|
||||||
|
|> Repo.delete()
|
||||||
|
|> elem(0)
|
||||||
|
|
||||||
|
{result, id}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl BaseMigrator
|
||||||
|
def retry_failed do
|
||||||
|
data_migration_id = data_migration_id()
|
||||||
|
|
||||||
|
failed_objects_query()
|
||||||
|
|> Repo.chunk_stream(100, :one)
|
||||||
|
|> Stream.each(fn object ->
|
||||||
|
with {res, _} when res != :error <- delete_context_object(object.id) do
|
||||||
|
_ =
|
||||||
|
Repo.query(
|
||||||
|
"DELETE FROM data_migration_failed_ids " <>
|
||||||
|
"WHERE data_migration_id = $1 AND record_id = $2",
|
||||||
|
[data_migration_id, object.id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Stream.run()
|
||||||
|
|
||||||
|
put_stat(:failed_count, failures_count())
|
||||||
|
persist_state()
|
||||||
|
|
||||||
|
force_continue()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp failed_objects_query do
|
||||||
|
from(o in Object)
|
||||||
|
|> join(:inner, [o], dmf in fragment("SELECT * FROM data_migration_failed_ids"),
|
||||||
|
on: dmf.record_id == o.id
|
||||||
|
)
|
||||||
|
|> where([_o, dmf], dmf.data_migration_id == ^data_migration_id())
|
||||||
|
|> order_by([o], asc: o.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -183,7 +183,7 @@ defmodule Pleroma.Migrators.HashtagsTableMigrator 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 @@ defmodule Pleroma.Notification 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 @@ defmodule Pleroma.Notification do
|
||||||
|> 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 @@ defmodule Pleroma.Notification do
|
||||||
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 @@ defmodule Pleroma.Notification 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 @@ defmodule Pleroma.Notification 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 @@ defmodule Pleroma.Notification do
|
||||||
(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 @@ defmodule Pleroma.Object do
|
||||||
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,
|
||||||
|
|
@ -145,7 +144,7 @@ defmodule Pleroma.Object do
|
||||||
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
|
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize(_, options \\ [fetch: false])
|
def normalize(_, options \\ [fetch: false, id_only: false])
|
||||||
|
|
||||||
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
|
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
|
||||||
# Use this whenever possible, especially when walking graphs in an O(N) loop!
|
# Use this whenever possible, especially when walking graphs in an O(N) loop!
|
||||||
|
|
@ -173,10 +172,15 @@ defmodule Pleroma.Object do
|
||||||
def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
|
def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
|
||||||
|
|
||||||
def normalize(ap_id, options) when is_binary(ap_id) do
|
def normalize(ap_id, options) when is_binary(ap_id) do
|
||||||
if Keyword.get(options, :fetch) do
|
cond do
|
||||||
Fetcher.fetch_object_from_id!(ap_id, options)
|
Keyword.get(options, :id_only) ->
|
||||||
else
|
ap_id
|
||||||
get_cached_by_ap_id(ap_id)
|
|
||||||
|
Keyword.get(options, :fetch) ->
|
||||||
|
Fetcher.fetch_object_from_id!(ap_id, options)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
get_cached_by_ap_id(ap_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -208,10 +212,6 @@ defmodule Pleroma.Object 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,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Object.Fetcher do
|
defmodule Pleroma.Object.Fetcher do
|
||||||
alias Pleroma.HTTP
|
alias Pleroma.HTTP
|
||||||
|
alias Pleroma.Instances
|
||||||
alias Pleroma.Maps
|
alias Pleroma.Maps
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Containment
|
alias Pleroma.Object.Containment
|
||||||
|
|
@ -26,8 +27,42 @@ defmodule Pleroma.Object.Fetcher 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
|
||||||
|
|
||||||
|
|
@ -200,6 +235,10 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
{:ok, body} <- get_object(id),
|
{:ok, body} <- get_object(id),
|
||||||
{:ok, data} <- safe_json_decode(body),
|
{:ok, data} <- safe_json_decode(body),
|
||||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||||
|
if not Instances.reachable?(id) do
|
||||||
|
Instances.set_reachable(id)
|
||||||
|
end
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
else
|
||||||
{:scheme, _} ->
|
{:scheme, _} ->
|
||||||
|
|
|
||||||
240
lib/pleroma/object/updater.ex
Normal file
240
lib/pleroma/object/updater.ex
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Object.Updater do
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
def update_content_fields(orig_object_data, updated_object) do
|
||||||
|
Pleroma.Constants.status_updatable_fields()
|
||||||
|
|> Enum.reduce(
|
||||||
|
%{data: orig_object_data, updated: false},
|
||||||
|
fn field, %{data: data, updated: updated} ->
|
||||||
|
updated =
|
||||||
|
updated or
|
||||||
|
(field != "updated" and
|
||||||
|
Map.get(updated_object, field) != Map.get(orig_object_data, field))
|
||||||
|
|
||||||
|
data =
|
||||||
|
if Map.has_key?(updated_object, field) do
|
||||||
|
Map.put(data, field, updated_object[field])
|
||||||
|
else
|
||||||
|
Map.drop(data, [field])
|
||||||
|
end
|
||||||
|
|
||||||
|
%{data: data, updated: updated}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_history(object) do
|
||||||
|
with history <- Map.get(object, "formerRepresentations"),
|
||||||
|
true <- is_map(history),
|
||||||
|
"OrderedCollection" <- Map.get(history, "type"),
|
||||||
|
true <- is_list(Map.get(history, "orderedItems")),
|
||||||
|
true <- is_integer(Map.get(history, "totalItems")) do
|
||||||
|
history
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_for(object) do
|
||||||
|
with history when not is_nil(history) <- maybe_history(object) do
|
||||||
|
history
|
||||||
|
else
|
||||||
|
_ -> history_skeleton()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp history_skeleton do
|
||||||
|
%{
|
||||||
|
"type" => "OrderedCollection",
|
||||||
|
"totalItems" => 0,
|
||||||
|
"orderedItems" => []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_update_history(
|
||||||
|
updated_object,
|
||||||
|
orig_object_data,
|
||||||
|
opts
|
||||||
|
) do
|
||||||
|
updated = opts[:updated]
|
||||||
|
use_history_in_new_object? = opts[:use_history_in_new_object?]
|
||||||
|
|
||||||
|
if not updated do
|
||||||
|
%{updated_object: updated_object, used_history_in_new_object?: false}
|
||||||
|
else
|
||||||
|
# Put edit history
|
||||||
|
# Note that we may have got the edit history by first fetching the object
|
||||||
|
{new_history, used_history_in_new_object?} =
|
||||||
|
with true <- use_history_in_new_object?,
|
||||||
|
updated_history when not is_nil(updated_history) <- maybe_history(opts[:new_data]) do
|
||||||
|
{updated_history, true}
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
history = history_for(orig_object_data)
|
||||||
|
|
||||||
|
latest_history_item =
|
||||||
|
orig_object_data
|
||||||
|
|> Map.drop(["id", "formerRepresentations"])
|
||||||
|
|
||||||
|
updated_history =
|
||||||
|
history
|
||||||
|
|> Map.put("orderedItems", [latest_history_item | history["orderedItems"]])
|
||||||
|
|> Map.put("totalItems", history["totalItems"] + 1)
|
||||||
|
|
||||||
|
{updated_history, false}
|
||||||
|
end
|
||||||
|
|
||||||
|
updated_object =
|
||||||
|
updated_object
|
||||||
|
|> Map.put("formerRepresentations", new_history)
|
||||||
|
|
||||||
|
%{updated_object: updated_object, used_history_in_new_object?: used_history_in_new_object?}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_poll(to_be_updated, updated_object) do
|
||||||
|
choice_key = fn data ->
|
||||||
|
if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
|
||||||
|
end
|
||||||
|
|
||||||
|
with true <- to_be_updated["type"] == "Question",
|
||||||
|
key <- choice_key.(updated_object),
|
||||||
|
true <- key == choice_key.(to_be_updated),
|
||||||
|
orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||||
|
new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||||
|
true <- orig_choices == new_choices do
|
||||||
|
# Choices are the same, but counts are different
|
||||||
|
to_be_updated
|
||||||
|
|> Map.put(key, updated_object[key])
|
||||||
|
else
|
||||||
|
# Choices (or vote type) have changed, do not allow this
|
||||||
|
_ -> to_be_updated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This calculates the data to be sent as the object of an Update.
|
||||||
|
# new_data's formerRepresentations is not considered.
|
||||||
|
# formerRepresentations is added to the returned data.
|
||||||
|
def make_update_object_data(original_data, new_data, date) do
|
||||||
|
%{data: updated_data, updated: updated} =
|
||||||
|
original_data
|
||||||
|
|> update_content_fields(new_data)
|
||||||
|
|
||||||
|
if not updated do
|
||||||
|
updated_data
|
||||||
|
else
|
||||||
|
%{updated_object: updated_data} =
|
||||||
|
updated_data
|
||||||
|
|> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false)
|
||||||
|
|
||||||
|
updated_data
|
||||||
|
|> Map.put("updated", date)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This calculates the data of the new Object from an Update.
|
||||||
|
# new_data's formerRepresentations is considered.
|
||||||
|
def make_new_object_data_from_update_object(original_data, new_data) do
|
||||||
|
update_is_reasonable =
|
||||||
|
with {_, updated} when not is_nil(updated) <- {:cur_updated, new_data["updated"]},
|
||||||
|
{_, {:ok, updated_time, _}} <- {:cur_updated, DateTime.from_iso8601(updated)},
|
||||||
|
{_, last_updated} when not is_nil(last_updated) <-
|
||||||
|
{:last_updated, original_data["updated"] || original_data["published"]},
|
||||||
|
{_, {:ok, last_updated_time, _}} <-
|
||||||
|
{:last_updated, DateTime.from_iso8601(last_updated)},
|
||||||
|
:gt <- DateTime.compare(updated_time, last_updated_time) do
|
||||||
|
:update_everything
|
||||||
|
else
|
||||||
|
# only allow poll updates
|
||||||
|
{:cur_updated, _} -> :no_content_update
|
||||||
|
:eq -> :no_content_update
|
||||||
|
# allow all updates
|
||||||
|
{:last_updated, _} -> :update_everything
|
||||||
|
# allow no updates
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
updated_object: updated_data,
|
||||||
|
used_history_in_new_object?: used_history_in_new_object?,
|
||||||
|
updated: updated
|
||||||
|
} =
|
||||||
|
if update_is_reasonable == :update_everything do
|
||||||
|
%{data: updated_data, updated: updated} =
|
||||||
|
original_data
|
||||||
|
|> update_content_fields(new_data)
|
||||||
|
|
||||||
|
updated_data
|
||||||
|
|> maybe_update_history(original_data,
|
||||||
|
updated: updated,
|
||||||
|
use_history_in_new_object?: true,
|
||||||
|
new_data: new_data
|
||||||
|
)
|
||||||
|
|> Map.put(:updated, updated)
|
||||||
|
else
|
||||||
|
%{
|
||||||
|
updated_object: original_data,
|
||||||
|
used_history_in_new_object?: false,
|
||||||
|
updated: false
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
updated_data =
|
||||||
|
if update_is_reasonable != false do
|
||||||
|
updated_data
|
||||||
|
|> maybe_update_poll(new_data)
|
||||||
|
else
|
||||||
|
updated_data
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
updated_data: updated_data,
|
||||||
|
updated: updated,
|
||||||
|
used_history_in_new_object?: used_history_in_new_object?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_each_history_item(%{"orderedItems" => items} = history, _object, fun) do
|
||||||
|
new_items =
|
||||||
|
Enum.map(items, fun)
|
||||||
|
|> Enum.reduce_while(
|
||||||
|
{:ok, []},
|
||||||
|
fn
|
||||||
|
{:ok, item}, {:ok, acc} -> {:cont, {:ok, acc ++ [item]}}
|
||||||
|
e, _acc -> {:halt, e}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
case new_items do
|
||||||
|
{:ok, items} -> {:ok, Map.put(history, "orderedItems", items)}
|
||||||
|
e -> e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def for_each_history_item(history, _, _) do
|
||||||
|
{:ok, history}
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_with_history(object, fun) do
|
||||||
|
with history <- object["formerRepresentations"],
|
||||||
|
object <- Map.drop(object, ["formerRepresentations"]),
|
||||||
|
{_, {:ok, object}} <- {:main_body, fun.(object)},
|
||||||
|
{_, {:ok, history}} <- {:history_items, for_each_history_item(history, object, fun)} do
|
||||||
|
object =
|
||||||
|
if history do
|
||||||
|
Map.put(object, "formerRepresentations", history)
|
||||||
|
else
|
||||||
|
object
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, object}
|
||||||
|
else
|
||||||
|
{:main_body, e} -> e
|
||||||
|
{:history_items, e} -> e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -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 @@ defmodule Pleroma.Signature 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),
|
||||||
|
|
@ -59,9 +66,8 @@ defmodule Pleroma.Signature do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign(%User{} = user, headers) do
|
def sign(%User{keys: keys} = user, headers) do
|
||||||
with {:ok, %{keys: keys}} <- User.ensure_keys_present(user),
|
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
|
||||||
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
|
|
||||||
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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 @@ defmodule Pleroma.Upload 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" => [
|
||||||
|
|
|
||||||
|
|
@ -711,6 +711,7 @@ defmodule Pleroma.User do
|
||||||
|> put_ap_id()
|
|> put_ap_id()
|
||||||
|> unique_constraint(:ap_id)
|
|> unique_constraint(:ap_id)
|
||||||
|> put_following_and_follower_and_featured_address()
|
|> put_following_and_follower_and_featured_address()
|
||||||
|
|> put_private_key()
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
|
|
@ -768,6 +769,7 @@ defmodule Pleroma.User do
|
||||||
|> put_ap_id()
|
|> put_ap_id()
|
||||||
|> unique_constraint(:ap_id)
|
|> unique_constraint(:ap_id)
|
||||||
|> put_following_and_follower_and_featured_address()
|
|> put_following_and_follower_and_featured_address()
|
||||||
|
|> put_private_key()
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_not_restricted_nickname(changeset, field) do
|
def validate_not_restricted_nickname(changeset, field) do
|
||||||
|
|
@ -846,6 +848,11 @@ defmodule Pleroma.User do
|
||||||
|> put_change(:featured_address, featured)
|
|> put_change(:featured_address, featured)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp put_private_key(changeset) do
|
||||||
|
{:ok, pem} = Keys.generate_rsa_pem()
|
||||||
|
put_change(changeset, :keys, pem)
|
||||||
|
end
|
||||||
|
|
||||||
defp autofollow_users(user) do
|
defp autofollow_users(user) do
|
||||||
candidates = Config.get([:instance, :autofollowed_nicknames])
|
candidates = Config.get([:instance, :autofollowed_nicknames])
|
||||||
|
|
||||||
|
|
@ -1574,13 +1581,19 @@ defmodule Pleroma.User 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)
|
||||||
|
|
@ -2080,6 +2093,7 @@ defmodule Pleroma.User do
|
||||||
follower_address: uri <> "/followers"
|
follower_address: uri <> "/followers"
|
||||||
}
|
}
|
||||||
|> change
|
|> change
|
||||||
|
|> put_private_key()
|
||||||
|> unique_constraint(:nickname)
|
|> unique_constraint(:nickname)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|> set_cache()
|
|> set_cache()
|
||||||
|
|
@ -2112,7 +2126,8 @@ defmodule Pleroma.User do
|
||||||
|
|
||||||
@doc "Gets or fetch a user by uri or nickname."
|
@doc "Gets or fetch a user by uri or nickname."
|
||||||
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||||
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
|
def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
|
||||||
|
def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
|
||||||
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
|
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
|
||||||
|
|
||||||
# wait a period of time and return newest version of the User structs
|
# wait a period of time and return newest version of the User structs
|
||||||
|
|
@ -2345,17 +2360,6 @@ defmodule Pleroma.User do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
|
|
||||||
|
|
||||||
def ensure_keys_present(%User{} = user) do
|
|
||||||
with {:ok, pem} <- Keys.generate_rsa_pem() do
|
|
||||||
user
|
|
||||||
|> cast(%{keys: pem}, [:keys])
|
|
||||||
|> validate_required([:keys])
|
|
||||||
|> update_and_set_cache()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_ap_ids_by_nicknames(nicknames) do
|
def get_ap_ids_by_nicknames(nicknames) do
|
||||||
from(u in User,
|
from(u in User,
|
||||||
where: u.nickname in ^nicknames,
|
where: u.nickname in ^nicknames,
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,7 @@ defmodule Pleroma.User.Backup do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(user, admin_id \\ nil) do
|
def create(user, admin_id \\ nil) do
|
||||||
with :ok <- validate_email_enabled(),
|
with :ok <- validate_limit(user, admin_id),
|
||||||
:ok <- validate_user_email(user),
|
|
||||||
:ok <- validate_limit(user, admin_id),
|
|
||||||
{:ok, backup} <- user |> new() |> Repo.insert() do
|
{:ok, backup} <- user |> new() |> Repo.insert() do
|
||||||
BackupWorker.process(backup, admin_id)
|
BackupWorker.process(backup, admin_id)
|
||||||
end
|
end
|
||||||
|
|
@ -86,20 +84,6 @@ defmodule Pleroma.User.Backup do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_email_enabled do
|
|
||||||
if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
{:error, dgettext("errors", "Backups require enabled email")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_user_email(%User{email: nil}) do
|
|
||||||
{:error, dgettext("errors", "Email is required")}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
|
|
||||||
|
|
||||||
def get_last(user_id) do
|
def get_last(user_id) do
|
||||||
__MODULE__
|
__MODULE__
|
||||||
|> where(user_id: ^user_id)
|
|> where(user_id: ^user_id)
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ defmodule Pleroma.User.Search do
|
||||||
|> subquery()
|
|> subquery()
|
||||||
|> order_by(desc: :search_rank)
|
|> order_by(desc: :search_rank)
|
||||||
|> maybe_restrict_local(for_user)
|
|> maybe_restrict_local(for_user)
|
||||||
|
|> filter_deactivated_users()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp select_top_users(query, top_user_ids) do
|
defp select_top_users(query, top_user_ids) do
|
||||||
|
|
@ -166,6 +167,10 @@ defmodule Pleroma.User.Search do
|
||||||
from(q in query, where: q.actor_type != "Application")
|
from(q in query, where: q.actor_type != "Application")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp filter_deactivated_users(query) do
|
||||||
|
from(q in query, where: q.is_active == true)
|
||||||
|
end
|
||||||
|
|
||||||
defp filter_blocked_user(query, %User{} = blocker) do
|
defp filter_blocked_user(query, %User{} = blocker) do
|
||||||
query
|
query
|
||||||
|> join(:left, [u], b in Pleroma.UserRelationship,
|
|> join(:left, [u], b in Pleroma.UserRelationship,
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,9 @@ defmodule Pleroma.UserRelationship do
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
@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)
|
||||||
|
|
@ -502,9 +511,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
|
||||||
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
|
||||||
|
includes_local_public = Map.get(opts, :includes_local_public, false)
|
||||||
|
|
||||||
opts = Map.delete(opts, :user)
|
opts = Map.delete(opts, :user)
|
||||||
|
|
||||||
[Constants.as_public()]
|
intended_recipients =
|
||||||
|
if includes_local_public do
|
||||||
|
[Constants.as_public(), as_local_public()]
|
||||||
|
else
|
||||||
|
[Constants.as_public()]
|
||||||
|
end
|
||||||
|
|
||||||
|
intended_recipients
|
||||||
|> fetch_activities_query(opts)
|
|> fetch_activities_query(opts)
|
||||||
|> restrict_unlisted(opts)
|
|> restrict_unlisted(opts)
|
||||||
|> fetch_paginated_optimized(opts, pagination)
|
|> fetch_paginated_optimized(opts, pagination)
|
||||||
|
|
@ -604,9 +622,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
do: query
|
do: query
|
||||||
|
|
||||||
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
|
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
|
||||||
|
local_public = as_local_public()
|
||||||
|
|
||||||
from(
|
from(
|
||||||
a in query,
|
a in query,
|
||||||
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
|
where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -693,8 +713,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
defp user_activities_recipients(%{godmode: true}), do: []
|
defp user_activities_recipients(%{godmode: true}), do: []
|
||||||
|
|
||||||
defp user_activities_recipients(%{reading_user: reading_user}) do
|
defp user_activities_recipients(%{reading_user: reading_user}) do
|
||||||
if reading_user do
|
if not is_nil(reading_user) and reading_user.local do
|
||||||
[Constants.as_public(), reading_user.ap_id | User.following(reading_user)]
|
[
|
||||||
|
Constants.as_public(),
|
||||||
|
as_local_public(),
|
||||||
|
reading_user.ap_id | User.following(reading_user)
|
||||||
|
]
|
||||||
else
|
else
|
||||||
[Constants.as_public()]
|
[Constants.as_public()]
|
||||||
end
|
end
|
||||||
|
|
@ -1135,8 +1159,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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
|
||||||
|
|
@ -1459,7 +1482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
|
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
|
||||||
defp normalize_image(_), do: nil
|
defp normalize_image(_), do: nil
|
||||||
|
|
||||||
defp object_to_user_data(data) do
|
defp object_to_user_data(data, additional) do
|
||||||
fields =
|
fields =
|
||||||
data
|
data
|
||||||
|> Map.get("attachment", [])
|
|> Map.get("attachment", [])
|
||||||
|
|
@ -1491,15 +1514,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
public_key =
|
public_key =
|
||||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||||
data["publicKey"]["publicKeyPem"]
|
data["publicKey"]["publicKeyPem"]
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_inbox =
|
shared_inbox =
|
||||||
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
|
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
|
||||||
data["endpoints"]["sharedInbox"]
|
data["endpoints"]["sharedInbox"]
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
birthday =
|
birthday =
|
||||||
|
|
@ -1508,13 +1527,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
{:ok, date} -> date
|
{:ok, date} -> date
|
||||||
{:error, _} -> nil
|
{:error, _} -> nil
|
||||||
end
|
end
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
show_birthday = !!birthday
|
show_birthday = !!birthday
|
||||||
|
|
||||||
user_data = %{
|
# if WebFinger request was already done, we probably have acct, otherwise
|
||||||
|
# we request WebFinger here
|
||||||
|
nickname = additional[:nickname_from_acct] || generate_nickname(data)
|
||||||
|
|
||||||
|
%{
|
||||||
ap_id: data["id"],
|
ap_id: data["id"],
|
||||||
uri: get_actor_url(data["url"]),
|
uri: get_actor_url(data["url"]),
|
||||||
ap_enabled: true,
|
ap_enabled: true,
|
||||||
|
|
@ -1536,23 +1557,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
inbox: data["inbox"],
|
inbox: data["inbox"],
|
||||||
shared_inbox: shared_inbox,
|
shared_inbox: shared_inbox,
|
||||||
accepts_chat_messages: accepts_chat_messages,
|
accepts_chat_messages: accepts_chat_messages,
|
||||||
pinned_objects: pinned_objects,
|
|
||||||
birthday: birthday,
|
birthday: birthday,
|
||||||
show_birthday: show_birthday
|
show_birthday: show_birthday,
|
||||||
|
pinned_objects: pinned_objects,
|
||||||
|
nickname: nickname
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# nickname can be nil because of virtual actors
|
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
|
||||||
if data["preferredUsername"] do
|
generated = "#{username}@#{URI.parse(data["id"]).host}"
|
||||||
Map.put(
|
|
||||||
user_data,
|
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
|
||||||
:nickname,
|
case WebFinger.finger(generated) do
|
||||||
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
|
{:ok, %{"subject" => "acct:" <> acct}} -> acct
|
||||||
)
|
_ -> generated
|
||||||
|
end
|
||||||
else
|
else
|
||||||
Map.put(user_data, :nickname, nil)
|
generated
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# nickname can be nil because of virtual actors
|
||||||
|
defp generate_nickname(_), do: nil
|
||||||
|
|
||||||
def fetch_follow_information_for_user(user) do
|
def fetch_follow_information_for_user(user) do
|
||||||
with {:ok, following_data} <-
|
with {:ok, following_data} <-
|
||||||
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
|
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
|
||||||
|
|
@ -1624,17 +1651,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
defp collection_private(_data), do: {:ok, true}
|
defp collection_private(_data), do: {:ok, true}
|
||||||
|
|
||||||
def user_data_from_user_object(data) do
|
def user_data_from_user_object(data, additional \\ []) do
|
||||||
with {:ok, data} <- MRF.filter(data) do
|
with {:ok, data} <- MRF.filter(data) do
|
||||||
{:ok, object_to_user_data(data)}
|
{:ok, object_to_user_data(data, additional)}
|
||||||
else
|
else
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_prepare_user_from_ap_id(ap_id) do
|
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
||||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||||
{:ok, data} <- user_data_from_user_object(data) do
|
{:ok, data} <- user_data_from_user_object(data, additional) do
|
||||||
{:ok, maybe_update_follow_information(data)}
|
{:ok, maybe_update_follow_information(data)}
|
||||||
else
|
else
|
||||||
# If this has been deleted, only log a debug and not an error
|
# If this has been deleted, only log a debug and not an error
|
||||||
|
|
@ -1712,13 +1739,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_user_from_ap_id(ap_id) do
|
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||||
user = User.get_cached_by_ap_id(ap_id)
|
user = User.get_cached_by_ap_id(ap_id)
|
||||||
|
|
||||||
if user && !User.ap_enabled?(user) do
|
if user && !User.ap_enabled?(user) do
|
||||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||||
else
|
else
|
||||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
|
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||||
|
|
||||||
if user do
|
if user do
|
||||||
|
|
@ -1738,8 +1765,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_user_from_nickname(nickname) do
|
def make_user_from_nickname(nickname) do
|
||||||
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
|
with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <-
|
||||||
make_user_from_ap_id(ap_id)
|
WebFinger.finger(nickname) do
|
||||||
|
make_user_from_ap_id(ap_id, nickname_from_acct: acct)
|
||||||
else
|
else
|
||||||
_e -> {:error, "No AP id in WebFinger"}
|
_e -> {:error, "No AP id in WebFinger"}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def user(conn, %{"nickname" => nickname}) do
|
def user(conn, %{"nickname" => nickname}) do
|
||||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
{:ok, user} <- User.ensure_keys_present(user) do
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|
|
@ -174,7 +173,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|
|
||||||
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
|
|
||||||
{:show_follows, true} <-
|
{:show_follows, true} <-
|
||||||
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do
|
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do
|
||||||
{page, _} = Integer.parse(page)
|
{page, _} = Integer.parse(page)
|
||||||
|
|
@ -192,8 +190,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
|
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|
|
@ -213,7 +210,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|
|
||||||
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
|
|
||||||
{:show_followers, true} <-
|
{:show_followers, true} <-
|
||||||
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do
|
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do
|
||||||
{page, _} = Integer.parse(page)
|
{page, _} = Integer.parse(page)
|
||||||
|
|
@ -231,8 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
|
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|
|
@ -245,8 +240,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
%{"nickname" => nickname, "page" => page?} = params
|
%{"nickname" => nickname, "page" => page?} = params
|
||||||
)
|
)
|
||||||
when page? in [true, "true"] do
|
when page? in [true, "true"] do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
{:ok, user} <- User.ensure_keys_present(user) do
|
|
||||||
# "include_poll_votes" is a hack because postgres generates inefficient
|
# "include_poll_votes" is a hack because postgres generates inefficient
|
||||||
# queries when filtering by 'Answer', poll votes will be hidden by the
|
# queries when filtering by 'Answer', poll votes will be hidden by the
|
||||||
# visibility filter in this case anyway
|
# visibility filter in this case anyway
|
||||||
|
|
@ -270,8 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def outbox(conn, %{"nickname" => nickname}) do
|
def outbox(conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
{:ok, user} <- User.ensure_keys_present(user) do
|
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_view(UserView)
|
|> put_view(UserView)
|
||||||
|
|
@ -328,14 +321,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp represent_service_actor(%User{} = user, conn) do
|
defp represent_service_actor(%User{} = user, conn) do
|
||||||
with {:ok, user} <- User.ensure_keys_present(user) do
|
conn
|
||||||
conn
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_view(UserView)
|
||||||
|> put_view(UserView)
|
|> render("user.json", %{user: user})
|
||||||
|> render("user.json", %{user: user})
|
|
||||||
else
|
|
||||||
nil -> {:error, :not_found}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp represent_service_actor(nil, _), do: {:error, :not_found}
|
defp represent_service_actor(nil, _), do: {:error, :not_found}
|
||||||
|
|
@ -388,12 +377,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
|
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
|
||||||
"nickname" => nickname
|
"nickname" => nickname
|
||||||
}) do
|
}) do
|
||||||
with {:ok, user} <- User.ensure_keys_present(user) do
|
conn
|
||||||
conn
|
|> put_resp_content_type("application/activity+json")
|
||||||
|> put_resp_content_type("application/activity+json")
|
|> put_view(UserView)
|
||||||
|> put_view(UserView)
|
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
|
||||||
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
||||||
|
|
@ -530,19 +517,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
|
|
||||||
{:ok, new_user} = User.ensure_keys_present(user)
|
|
||||||
|
|
||||||
for_user =
|
|
||||||
if new_user != user and match?(%User{}, for_user) do
|
|
||||||
User.get_cached_by_nickname(for_user.nickname)
|
|
||||||
else
|
|
||||||
for_user
|
|
||||||
end
|
|
||||||
|
|
||||||
{new_user, for_user}
|
|
||||||
end
|
|
||||||
|
|
||||||
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
||||||
with {:ok, object} <-
|
with {:ok, object} <-
|
||||||
ActivityPub.upload(
|
ActivityPub.upload(
|
||||||
|
|
|
||||||
|
|
@ -218,10 +218,16 @@ defmodule Pleroma.Web.ActivityPub.Builder 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 @@ defmodule Pleroma.Web.ActivityPub.Builder 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
|
||||||
|
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
|
||||||
|
|
||||||
defp check_ftl_removal(message, _hashtags), do: {:ok, message}
|
defp check_ftl_removal(message, _hashtags), do: {:ok, message}
|
||||||
|
|
||||||
defp check_sensitive(message, hashtags) do
|
defp check_sensitive(message) do
|
||||||
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
|
{:ok, new_object} =
|
||||||
{:ok, Kernel.put_in(message, ["object", "sensitive"], true)}
|
Object.Updater.do_with_history(message["object"], fn object ->
|
||||||
else
|
hashtags = Object.hashtags(%Object{data: object})
|
||||||
{:ok, message}
|
|
||||||
end
|
if Enum.any?(Config.get([:mrf_hashtag, :sensitive]), fn match -> match in hashtags end) do
|
||||||
|
{:ok, Map.put(object, "sensitive", true)}
|
||||||
|
else
|
||||||
|
{:ok, object}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, Map.put(message, "object", new_object)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def filter(%{"type" => "Create", "object" => object} = message) do
|
def filter(%{"type" => type, "object" => object} = message) when type in ["Create", "Update"] do
|
||||||
hashtags = Object.hashtags(%Object{data: object})
|
history_items =
|
||||||
|
with %{"formerRepresentations" => %{"orderedItems" => items}} <- object do
|
||||||
|
items
|
||||||
|
else
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
|
||||||
|
historical_hashtags =
|
||||||
|
Enum.reduce(history_items, [], fn item, acc ->
|
||||||
|
acc ++ Object.hashtags(%Object{data: item})
|
||||||
|
end)
|
||||||
|
|
||||||
|
hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags
|
||||||
|
|
||||||
if hashtags != [] do
|
if hashtags != [] do
|
||||||
with {:ok, message} <- check_reject(message, hashtags),
|
with {:ok, message} <- check_reject(message, hashtags),
|
||||||
{:ok, message} <- check_ftl_removal(message, hashtags),
|
{:ok, message} <-
|
||||||
{:ok, message} <- check_sensitive(message, hashtags) do
|
(if "type" == "Create" do
|
||||||
|
check_ftl_removal(message, hashtags)
|
||||||
|
else
|
||||||
|
{:ok, message}
|
||||||
|
end),
|
||||||
|
{:ok, message} <- check_sensitive(message) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -27,24 +27,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_reject(%{"object" => %{} = object} = message) do
|
defp check_reject(%{"object" => %{} = object} = message) do
|
||||||
payload = object_payload(object)
|
with {:ok, _new_object} <-
|
||||||
|
Pleroma.Object.Updater.do_with_history(object, fn object ->
|
||||||
|
payload = object_payload(object)
|
||||||
|
|
||||||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||||
string_matches?(payload, pattern)
|
string_matches?(payload, pattern)
|
||||||
end) do
|
end) do
|
||||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||||
else
|
else
|
||||||
|
{:ok, message}
|
||||||
|
end
|
||||||
|
end) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
|
else
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_ftl_removal(%{"to" => to, "object" => %{} = object} = message) do
|
defp check_ftl_removal(%{"type" => "Create", "to" => to, "object" => %{} = object} = message) do
|
||||||
payload = object_payload(object)
|
check_keyword = fn object ->
|
||||||
|
payload = object_payload(object)
|
||||||
|
|
||||||
if Pleroma.Constants.as_public() in to and
|
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
|
||||||
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
|
|
||||||
string_matches?(payload, pattern)
|
string_matches?(payload, pattern)
|
||||||
end) do
|
end) do
|
||||||
|
{:should_delist, nil}
|
||||||
|
else
|
||||||
|
{:ok, %{}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should_delist? = fn object ->
|
||||||
|
with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check_keyword) do
|
||||||
|
false
|
||||||
|
else
|
||||||
|
_ -> true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if Pleroma.Constants.as_public() in to and should_delist?.(object) do
|
||||||
to = List.delete(to, Pleroma.Constants.as_public())
|
to = List.delete(to, Pleroma.Constants.as_public())
|
||||||
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
|
||||||
|
|
||||||
|
|
@ -59,8 +81,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
||||||
|
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
|
||||||
|
|
||||||
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 =
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
|
||||||
type: {:list, :atom},
|
type: {:list, :atom},
|
||||||
description:
|
description:
|
||||||
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
|
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
|
||||||
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
|
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines, additionally for followers-only it degrades to a direct message; " <>
|
||||||
"`:reject` rejects the message entirely",
|
"`:reject` rejects the message entirely",
|
||||||
suggestions: [:delist, :strip_followers, :reject]
|
suggestions: [:delist, :strip_followers, :reject]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
meta
|
meta
|
||||||
)
|
)
|
||||||
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
||||||
with {:ok, object_data} <- cast_and_apply(object),
|
with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
|
||||||
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
|
meta = Keyword.put(meta, :object_data, object_data),
|
||||||
{:ok, create_activity} <-
|
{:ok, create_activity} <-
|
||||||
create_activity
|
create_activity
|
||||||
|> CreateGenericValidator.cast_and_validate(meta)
|
|> CreateGenericValidator.cast_and_validate(meta)
|
||||||
|
|
@ -128,19 +128,53 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
end
|
end
|
||||||
|
|
||||||
with {:ok, object} <-
|
with {:ok, object} <-
|
||||||
object
|
do_separate_with_history(object, fn object ->
|
||||||
|> validator.cast_and_validate()
|
with {:ok, object} <-
|
||||||
|> Ecto.Changeset.apply_action(:insert) do
|
object
|
||||||
object = stringify_keys(object)
|
|> validator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
object = stringify_keys(object)
|
||||||
|
|
||||||
# Insert copy of hashtags as strings for the non-hashtag table indexing
|
# Insert copy of hashtags as strings for the non-hashtag table indexing
|
||||||
tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
|
tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
|
||||||
object = Map.put(object, "tag", tag)
|
object = Map.put(object, "tag", tag)
|
||||||
|
|
||||||
|
{:ok, object}
|
||||||
|
end
|
||||||
|
end) do
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate(
|
||||||
|
%{"type" => "Update", "object" => %{"type" => objtype} = object} = update_activity,
|
||||||
|
meta
|
||||||
|
)
|
||||||
|
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
||||||
|
with {_, false} <- {:local, Access.get(meta, :local, false)},
|
||||||
|
{_, {:ok, object_data, _}} <- {:object_validation, validate(object, meta)},
|
||||||
|
meta = Keyword.put(meta, :object_data, object_data),
|
||||||
|
{:ok, update_activity} <-
|
||||||
|
update_activity
|
||||||
|
|> UpdateValidator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
update_activity = stringify_keys(update_activity)
|
||||||
|
{:ok, update_activity, meta}
|
||||||
|
else
|
||||||
|
{:local, _} ->
|
||||||
|
with {:ok, object} <-
|
||||||
|
update_activity
|
||||||
|
|> UpdateValidator.cast_and_validate()
|
||||||
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
|
object = stringify_keys(object)
|
||||||
|
{:ok, object, meta}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:object_validation, e} ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def validate(%{"type" => type} = object, meta)
|
def validate(%{"type" => type} = object, meta)
|
||||||
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
|
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
|
||||||
ChatMessage Answer] do
|
ChatMessage Answer] do
|
||||||
|
|
@ -178,6 +212,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator 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
|
||||||
|
|
@ -204,8 +247,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
|
|
||||||
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
|
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
|
||||||
|
|
||||||
# is_struct/1 appears in Elixir 1.11
|
def stringify_keys(object) when is_struct(object) do
|
||||||
def stringify_keys(%{__struct__: _} = object) do
|
|
||||||
object
|
object
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|> stringify_keys
|
|> stringify_keys
|
||||||
|
|
@ -236,4 +278,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
||||||
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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator 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,23 +154,26 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
|
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
# - Update the user
|
# - Update the user
|
||||||
|
# - Update a non-user object (Note, Question, etc.)
|
||||||
#
|
#
|
||||||
# For a local user, we also get a changeset with the full information, so we
|
# For a local user, we also get a changeset with the full information, so we
|
||||||
# can update non-federating, non-activitypub settings as well.
|
# can update non-federating, non-activitypub settings as well.
|
||||||
@impl true
|
@impl true
|
||||||
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
|
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
|
||||||
if changeset = Keyword.get(meta, :user_update_changeset) do
|
updated_object_id = updated_object["id"]
|
||||||
changeset
|
|
||||||
|> User.update_and_set_cache()
|
with {_, true} <- {:has_id, is_binary(updated_object_id)},
|
||||||
|
%{"type" => type} <- updated_object,
|
||||||
|
{_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do
|
||||||
|
if is_user do
|
||||||
|
handle_update_user(object, meta)
|
||||||
|
else
|
||||||
|
handle_update_object(object, meta)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
|
_ ->
|
||||||
|
{:ok, object, meta}
|
||||||
User.get_by_ap_id(updated_object["id"])
|
|
||||||
|> User.remote_user_changeset(new_user_data)
|
|
||||||
|> User.update_and_set_cache()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, object, meta}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
|
|
@ -278,7 +282,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
# Tasks this handles:
|
# Tasks this handles:
|
||||||
# - Delete and unpins the create activity
|
# - Delete and unpins the create activity
|
||||||
# - Replace object with Tombstone
|
# - Replace object with Tombstone
|
||||||
# - Set up notification
|
|
||||||
# - Reduce the user note count
|
# - Reduce the user note count
|
||||||
# - Reduce the reply count
|
# - Reduce the reply count
|
||||||
# - Stream out the activity
|
# - Stream out the activity
|
||||||
|
|
@ -320,7 +323,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
end
|
end
|
||||||
|
|
||||||
if result == :ok do
|
if result == :ok do
|
||||||
Notification.create_notifications(object)
|
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
else
|
else
|
||||||
{:error, result}
|
{:error, result}
|
||||||
|
|
@ -390,6 +392,79 @@ defmodule Pleroma.Web.ActivityPub.SideEffects 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
{: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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
|> 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
|> 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
|
||||||
|
|
@ -767,22 +748,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
ActivityPub.fetch_activities([], params, :offset)
|
ActivityPub.fetch_activities([], params, :offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_report_state(%Activity{} = activity, state)
|
defp maybe_strip_report_status(data, state) do
|
||||||
when state in @strip_status_report_states do
|
with true <- Config.get([:instance, :report_strip_status]),
|
||||||
{:ok, stripped_activity} = strip_report_status_data(activity)
|
true <- state in @strip_status_report_states,
|
||||||
|
{:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do
|
||||||
new_data =
|
data |> Map.put("object", stripped_activity.data["object"])
|
||||||
activity.data
|
else
|
||||||
|> Map.put("state", state)
|
_ -> data
|
||||||
|> Map.put("object", stripped_activity.data["object"])
|
end
|
||||||
|
|
||||||
activity
|
|
||||||
|> Changeset.change(data: new_data)
|
|
||||||
|> Repo.update()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
|
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
|
||||||
new_data = Map.put(activity.data, "state", state)
|
new_data =
|
||||||
|
activity.data
|
||||||
|
|> Map.put("state", state)
|
||||||
|
|> maybe_strip_report_status(state)
|
||||||
|
|
||||||
activity
|
activity
|
||||||
|> Changeset.change(data: new_data)
|
|> Changeset.change(data: new_data)
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
||||||
|
|
||||||
def render("object.json", %{object: %Activity{} = activity}) do
|
def render("object.json", %{object: %Activity{} = activity}) do
|
||||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
|
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
|
||||||
object = Object.normalize(activity, fetch: false)
|
object_id = Object.normalize(activity, id_only: true)
|
||||||
|
|
||||||
additional =
|
additional =
|
||||||
Transmogrifier.prepare_object(activity.data)
|
Transmogrifier.prepare_object(activity.data)
|
||||||
|> Map.put("object", object.data["id"])
|
|> Map.put("object", object_id)
|
||||||
|
|
||||||
Map.merge(base, additional)
|
Map.merge(base, additional)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
||||||
def render("endpoints.json", _), do: %{}
|
def render("endpoints.json", _), do: %{}
|
||||||
|
|
||||||
def render("service.json", %{user: user}) do
|
def render("service.json", %{user: user}) do
|
||||||
{:ok, user} = User.ensure_keys_present(user)
|
|
||||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||||
public_key = :public_key.pem_encode([public_key])
|
public_key = :public_key.pem_encode([public_key])
|
||||||
|
|
@ -71,7 +70,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
||||||
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
||||||
|
|
||||||
def render("user.json", %{user: user}) do
|
def render("user.json", %{user: user}) do
|
||||||
{:ok, user} = User.ensure_keys_present(user)
|
|
||||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||||
public_key = :public_key.pem_encode([public_key])
|
public_key = :public_key.pem_encode([public_key])
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
||||||
when module in [Activity, Object] do
|
when module in [Activity, Object] do
|
||||||
x = [user.ap_id | User.following(user)]
|
x = [user.ap_id | User.following(user)]
|
||||||
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
|
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
|
||||||
is_public?(message) || Enum.any?(x, &(&1 in y))
|
|
||||||
|
user_is_local = user.local
|
||||||
|
federatable = not is_local_public?(message)
|
||||||
|
(is_public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
|
||||||
end
|
end
|
||||||
|
|
||||||
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ defmodule Pleroma.Web.AdminAPI.ChatController do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Chat
|
alias Pleroma.Chat
|
||||||
alias Pleroma.Chat.MessageReference
|
alias Pleroma.Chat.MessageReference
|
||||||
alias Pleroma.ModerationLog
|
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
alias Pleroma.Web.AdminAPI
|
alias Pleroma.Web.AdminAPI
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
@ -42,12 +41,6 @@ defmodule Pleroma.Web.AdminAPI.ChatController do
|
||||||
^chat_id <- to_string(cm_ref.chat_id),
|
^chat_id <- to_string(cm_ref.chat_id),
|
||||||
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
|
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
|
||||||
{:ok, _} <- CommonAPI.delete(activity_id, user) do
|
{:ok, _} <- CommonAPI.delete(activity_id, user) do
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
action: "chat_message_delete",
|
|
||||||
actor: user,
|
|
||||||
subject_id: message_id
|
|
||||||
})
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(MessageReferenceView)
|
|> put_view(MessageReferenceView)
|
||||||
|> render("show.json", chat_message_reference: cm_ref)
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,6 @@ defmodule Pleroma.Web.AdminAPI.StatusController do
|
||||||
|
|
||||||
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
|
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
action: "status_delete",
|
|
||||||
actor: user,
|
|
||||||
subject_id: id
|
|
||||||
})
|
|
||||||
|
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.AdminAPI.Report do
|
defmodule Pleroma.Web.AdminAPI.Report do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
||||||
def extract_report_info(
|
def extract_report_info(
|
||||||
|
|
@ -16,10 +17,42 @@ defmodule Pleroma.Web.AdminAPI.Report do
|
||||||
status_ap_ids
|
status_ap_ids
|
||||||
|> Enum.reject(&is_nil(&1))
|
|> Enum.reject(&is_nil(&1))
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
act when is_map(act) -> Activity.get_by_ap_id_with_object(act["id"])
|
act when is_map(act) ->
|
||||||
act when is_binary(act) -> Activity.get_by_ap_id_with_object(act)
|
Activity.get_by_ap_id_with_object(act["id"]) || make_fake_activity(act, user)
|
||||||
|
|
||||||
|
act when is_binary(act) ->
|
||||||
|
Activity.get_by_ap_id_with_object(act)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
%{report: report, user: user, account: account, statuses: statuses}
|
%{report: report, user: user, account: account, statuses: statuses}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp make_fake_activity(act, user) do
|
||||||
|
%Activity{
|
||||||
|
id: "pleroma:fake",
|
||||||
|
data: %{
|
||||||
|
"actor" => user.ap_id,
|
||||||
|
"type" => "Create",
|
||||||
|
"to" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"object" => act["id"],
|
||||||
|
"published" => act["published"],
|
||||||
|
"id" => act["id"],
|
||||||
|
"context" => "pleroma:fake"
|
||||||
|
},
|
||||||
|
recipients: [user.ap_id],
|
||||||
|
object: %Object{
|
||||||
|
data: %{
|
||||||
|
"actor" => user.ap_id,
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => act["content"],
|
||||||
|
"published" => act["published"],
|
||||||
|
"to" => [],
|
||||||
|
"cc" => [],
|
||||||
|
"id" => act["id"],
|
||||||
|
"context" => "pleroma:fake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,22 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_from_followers_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Account actions"],
|
||||||
|
summary: "Remove from followers",
|
||||||
|
operationId: "AccountController.remove_from_followers",
|
||||||
|
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||||
|
description: "Remove the given account from followers",
|
||||||
|
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||||
|
400 => Operation.response("Error", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Error", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def note_operation do
|
def note_operation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["Account actions"],
|
tags: ["Account actions"],
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["Backups"],
|
tags: ["Backups"],
|
||||||
summary: "List backups",
|
summary: "List backups",
|
||||||
security: [%{"oAuth" => ["read:account"]}],
|
security: [%{"oAuth" => ["read:backups"]}],
|
||||||
operationId: "PleromaAPI.BackupController.index",
|
operationId: "PleromaAPI.BackupController.index",
|
||||||
responses: %{
|
responses: %{
|
||||||
200 =>
|
200 =>
|
||||||
|
|
@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["Backups"],
|
tags: ["Backups"],
|
||||||
summary: "Create a backup",
|
summary: "Create a backup",
|
||||||
security: [%{"oAuth" => ["read:account"]}],
|
security: [%{"oAuth" => ["read:backups"]}],
|
||||||
operationId: "PleromaAPI.BackupController.create",
|
operationId: "PleromaAPI.BackupController.create",
|
||||||
responses: %{
|
responses: %{
|
||||||
200 =>
|
200 =>
|
||||||
|
|
|
||||||
|
|
@ -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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation 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" => [],
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Conversation.Participation
|
alias Pleroma.Conversation.Participation
|
||||||
alias Pleroma.Formatter
|
alias Pleroma.Formatter
|
||||||
|
alias Pleroma.ModerationLog
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.ThreadMute
|
alias Pleroma.ThreadMute
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
|
@ -147,6 +148,21 @@ defmodule Pleroma.Web.CommonAPI do
|
||||||
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
||||||
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
||||||
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
||||||
|
if User.superuser?(user) and user.ap_id != object.data["actor"] do
|
||||||
|
action =
|
||||||
|
if object.data["type"] == "ChatMessage" do
|
||||||
|
"chat_message_delete"
|
||||||
|
else
|
||||||
|
"status_delete"
|
||||||
|
end
|
||||||
|
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
action: action,
|
||||||
|
actor: user,
|
||||||
|
subject_id: activity_id
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
{:ok, delete}
|
{:ok, delete}
|
||||||
else
|
else
|
||||||
{:find_activity, _} ->
|
{:find_activity, _} ->
|
||||||
|
|
@ -402,6 +418,41 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,15 @@ defmodule Pleroma.Web.Federator do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def publish(activity) do
|
def publish(%Pleroma.Activity{data: %{"type" => type}} = activity) do
|
||||||
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
|
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id},
|
||||||
|
priority: publish_priority(type)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp publish_priority("Delete"), do: 3
|
||||||
|
defp publish_priority(_), do: 0
|
||||||
|
|
||||||
# Job Worker Callbacks
|
# Job Worker Callbacks
|
||||||
|
|
||||||
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
||||||
|
|
@ -61,10 +66,8 @@ defmodule Pleroma.Web.Federator do
|
||||||
def perform(:publish, activity) do
|
def perform(:publish, activity) do
|
||||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||||
|
|
||||||
with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
|
%User{} = actor = User.get_cached_by_ap_id(activity.data["actor"])
|
||||||
{:ok, actor} <- User.ensure_keys_present(actor) do
|
Publisher.publish(actor, activity)
|
||||||
Publisher.publish(actor, activity)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(:incoming_ap_doc, params) do
|
def perform(:incoming_ap_doc, params) do
|
||||||
|
|
|
||||||
|
|
@ -76,16 +76,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
|
%{scopes: ["follow", "write:follows"]}
|
||||||
|
when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
|
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
|
||||||
|
|
||||||
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
|
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
|
||||||
|
|
||||||
@relationship_actions [:follow, :unfollow]
|
@relationship_actions [:follow, :unfollow, :remove_from_followers]
|
||||||
@needs_account ~W(
|
@needs_account ~W(
|
||||||
followers following lists follow unfollow mute unmute block unblock note endorse unendorse
|
followers following lists follow unfollow mute unmute block unblock
|
||||||
|
note endorse unendorse remove_from_followers
|
||||||
)a
|
)a
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
|
@ -477,6 +479,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "POST /api/v1/accounts/:id/remove_from_followers"
|
||||||
|
def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
|
||||||
|
{:error, "Can not unfollow yourself"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
|
||||||
|
with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
|
||||||
|
render(conn, "relationship.json", user: followed, target: follower)
|
||||||
|
else
|
||||||
|
nil ->
|
||||||
|
render_error(conn, :not_found, "Record not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc "POST /api/v1/follows"
|
@doc "POST /api/v1/follows"
|
||||||
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
|
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
|
||||||
case User.get_cached_by_nickname(uri) do
|
case User.get_cached_by_nickname(uri) do
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|> Map.put(:muting_user, user)
|
|> Map.put(:muting_user, user)
|
||||||
|> Map.put(:reply_filtering_user, user)
|
|> Map.put(:reply_filtering_user, user)
|
||||||
|> Map.put(:instance, params[:instance])
|
|> Map.put(:instance, params[:instance])
|
||||||
|
# Restricts unfederated content to authenticated users
|
||||||
|
|> Map.put(:includes_local_public, not is_nil(user))
|
||||||
|> ActivityPub.fetch_public_activities()
|
|> ActivityPub.fetch_public_activities()
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView 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 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView 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
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue